diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000000..f731f7194a
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,6 @@
+# root = true
+
+# Unix-style newlines with a newline ending every file
+# [*]
+# indent_style = tab
+# indent_size = 4
diff --git a/.gitignore b/.gitignore
index 2938b3683b..85bd82b7ff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -278,6 +278,7 @@ $RECYCLE.BIN/
/src/Plugins/*-sym
/src/Tests/*-sym
/src/Presentation/SmartStore.Web/Themes/*-sym
+/src/Presentation/SmartStore.Web/Themes/HP
###########
@@ -294,16 +295,17 @@ Kopie von*
/build
/src/Presentation/SmartStore.Web/Plugins
-/src/Presentation/SmartStore.Web/App_Data/Tenants/
/src/Presentation/SmartStore.Web/App_Data/_temp/
/src/Presentation/SmartStore.Web/App_Data/ExportProfiles/
/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
/src/Presentation/SmartStore.Web/Media
+/src/**/App_Data/Tenants/
# Import/Export
/src/Presentation/SmartStore.Web/Content/files/[Ee]xport[Ii]mport/*.*
@@ -319,3 +321,4 @@ Kopie von*
src/SmartStoreNET.Packager.sln
Log.txt
+src/Presentation/SmartStore.Web/Themes/FlexMuseo/
diff --git a/CREDITS.txt b/CREDITS.txt
index 88b2d0a6be..5ea4c4a6fb 100644
--- a/CREDITS.txt
+++ b/CREDITS.txt
@@ -1,11 +1,8 @@
SmartStore.NET
-Copyright 1999-2014 SmartStore AG
+Copyright 1999-2018 SmartStore AG
http://www.smartstore.com | https://github.com/smartstoreag/SmartStoreNET
-SmartStore.NET is a fork of the ASP.NET open source e-commerce solution
-nopCommerce (http://www.nopcommerce.com).
-
SmartStore.NET includes works distributed under the licenses listed below.
Please refer to the specific resources for more detailed information about the authors,
copyright notices, and licenses.
@@ -14,7 +11,7 @@ copyright notices, and licenses.
51Degrees
---------------------------------------------
WebSite: http://51degrees.mobi/
-Copyright: Copyright 2010 - 2013 51Degrees.mobi Limited
+Copyright: Copyright 2010 - 2018 51Degrees.mobi Limited
License: Mozilla Public License 2.0 (MPL-2.0)
@@ -28,7 +25,7 @@ License: BSD License
ASP.NET MVC
---------------------------------------------
-Website: http://aspnet.codeplex.com/wikipage?title=MVC&referringTitle=Home
+Website: https://github.com/aspnet/AspNetWebStack
Copyright: Copyright (c) 2008-2011 Microsoft Corporation
License: MICROSOFT ASP.NET MODEL VIEW CONTROLLER 3 EULA
http://go.microsoft.com/fwlink/?LinkID=207621
@@ -36,26 +33,32 @@ License: MICROSOFT ASP.NET MODEL VIEW CONTROLLER 3 EULA
Autofac
---------------------------------------------
-Website: http://code.google.com/p/autofac/
-Copyright: Copyright (c) 2007-2014 Autofac Contributors
- http://code.google.com/p/autofac/wiki/Contributing
+Website: https://autofac.org/
+Copyright: Copyright (c) 2007-2018 Autofac Contributors
License: MIT
AutoMapper
---------------------------------------------
WebSite: http://www.automapper.org
-Copyright: Copyright 2008-2013 Jimmy Bogard and other contributors
+Copyright: Copyright 2008-2018 Jimmy Bogard and other contributors
License: MIT
Bundle Transformer
---------------------------------------------
-Website: https://bundletransformer.codeplex.com
+Website: https://github.com/Taritsyn/BundleTransformer
Copyright: Copyright Andrey Taritsyn 2014
License: Apache License 2.0 (Apache)
+summernote
+---------------------------------------------
+WebSite: https://summernote.org/
+Copyright: Copyright (c) 2015~ Summernote Team (https://github.com/orgs/summernote/people)
+License: MIT
+
+
DotNetOpenAuth
---------------------------------------------
Website: http://dotnetopenauth.net/
@@ -65,34 +68,28 @@ License: MS-PL
DotNetZip
---------------------------------------------
-Website: http://dotnetzip.codeplex.com/
+Website: https://archive.codeplex.com/?p=dotnetzip
Copyright:
License: MS-PL
Entity Framework
---------------------------------------------
-Website: https://entityframework.codeplex.com
-Copyright: Copyright (c) 2012-2014 Microsoft Corporation
+Website: https://github.com/aspnet/EntityFramework6
+Copyright: Copyright (c) 2012-2018 Microsoft Corporation
License: Apache License 2.0 (Apache)
EPPlus
---------------------------------------------
-Website: https://epplus.codeplex.com/
+Website: https://github.com/JanKallman/EPPlus
Copyright: Copyright (C) 2011 Jan Kllman
License: GNU Library General Public License (LGPL)
-Fasterflect
----------------------------------------------
-Website: https://fasterflect.codeplex.com/
-License: Apache License 2.0 (Apache)
-
-
Fluent Validation
---------------------------------------------
-Website: https://fluentvalidation.codeplex.com/
+Website: https://github.com/JeremySkinner/FluentValidation
License: Apache License 2.0 (Apache)
@@ -103,11 +100,11 @@ Copyright: Copyright (C) 2014 Glimpse contributors (http://getglimpse.com/Co
License: Apache License 2.0 (Apache)
-ImageResizer.NET
+ImageProcessor
---------------------------------------------
-Website: https://imageresizing.net/
-Copyright: Copyright (c) 2012 Imazen
-License: Freedom License
+Website: http://imageprocessor.org/
+Copyright: Copyright (c) 2018 James Jackson-South
+License: Apache License 2.0 (Apache)
JavaScriptEngineSwitcher
@@ -131,16 +128,9 @@ Copyright: Copyright (c) 2007-2009 Ariel Flesler
License: MIT
-jQuery UI (Core, Widget, Datepicker)
----------------------------------------------
-Website: http://docs.jquery.com/UI
-Copyright: Copyright (c) 2010 lib/jquery.ui/AUTHORS.txt (http://jqueryui.com/about)
-License: MIT
-
-
-JSON.NET
+Json.NET
---------------------------------------------
-Website: http://james.newtonking.com/
+Website: https://www.newtonsoft.com/json
Copyright: Copyright (c) 2007 James Newton-King
License: MIT
@@ -152,6 +142,13 @@ Copyright: Copyright (c) 2014 Maxmind
License: Creative Commons Attribution-ShareAlike 3.0 Unported License
+Linearicons by Perxis
+---------------------------------------------
+Website: https://linearicons.com
+Copyright: Copyright (c) 2015 Linearicons.com (https://perxis.com/)
+License: Dual (CC BY-SA 4.0 and commercial)
+
+
Microsoft SQL Server Compact 4.0
---------------------------------------------
Website: http://www.microsoft.com
@@ -173,35 +170,49 @@ License: MIT
NuGet
---------------------------------------------
-Website: http://nuget.codeplex.com
-Copyright: Copyright 2010-2011 Outercurve Foundation
+Website: https://github.com/nuget/home
+Copyright: Copyright 2010-2018 Outercurve Foundation
License: Apache Software Foundation License 2.0
nUnit
---------------------------------------------
-Website: http://www.nunit.org/index.php
+Website: http://nunit.org/
Copyright: Copyright (c) 2002-2007 NUnit.org
- Portions Copyright (c) 2002-2008 Charlie Poole or Copyright (c) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov or Copyright (c) 2000-2002 Philip A. Craig
+ Portions Copyright (c) 2002-2018 Charlie Poole or Copyright (c) 2002-2004 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov or Copyright (c) 2000-2002 Philip A. Craig
License: Derived from zlib: http://nunit.org/index.php?p=license&r=2.4
+PhotoSwipe
+---------------------------------------------
+Website: http://photoswipe.com/
+Copyright: Copyright (c) 2014-2018 Dmitry Semenov, http://dimsemenov.com
+License: MIT
+
+
+slick carousel
+---------------------------------------------
+Website: http://kenwheeler.github.io/slick/
+Copyright: Copyright (c) 2018 Ken Wheeler
+License: MIT
+
+
Telerik MVC Extensions
---------------------------------------------
Website: http://www.telerik.com/
License: GPL v3.0
-TinyMCE
+Twitter Bootstrap
---------------------------------------------
-WebSite: http://tinymce.moxiecode.com/
-Copyright: Copyright (c) 2003-2009 Moxiecode Systems AB
-License: LGPL 2.1
+Website: https://getbootstrap.com/
+License: Apache License v2.0
-Twitter Bootstrap
+ua_parser
---------------------------------------------
-Website: http://twitter.github.io/bootstrap/
+Website: https://github.com/ua-parser/uap-csharp
+Copyright: Copyright 2012 Sren Enemrke
License: Apache License v2.0
diff --git a/README.md b/README.md
index f6fc1133c9..f8e84aa9bb 100644
--- a/README.md
+++ b/README.md
@@ -1,23 +1,60 @@
-## Overview
+
+
+
+
+
+
+
+
+
+
+
+
+Ready. Sell. Grow.
+
-
+
+
+
+
+
+
-SmartStore.NET is a free, open source, full-featured e-commerce solution for companies of any size. It is web standards compliant and incorporates the newest Microsoft web technology stack.
+
+ Try Online ∙
+ Docs ∙
+ Forum ∙
+ Marketplace ∙
+ Translations
+
+
-**SmartStore.NET 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 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.
-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... totally FREE!
+**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 delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Twitter Bootstrap`, `Less CSS` and others. The innovative ShopBar, a powerful layered navigation, reviews & ratings, an optimized checkout process... they all provide a perfect shopping experience for your buyers.
+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.
-The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-)
+
+
+
+
+
+
+**Smartstore.NET delivers a beautiful and configurable shop front-end out-of-the-box**, built with a design approach on the highest level, including components like `Bootstrap 4`, `Sass` and others. The supplied theme _Flex_ is modern, clean and fully responsive, giving buyers the best possible shopping experience on any device.
+
+The state-of-the-art architecture of Smartstore.NET - with `ASP.NET 4.5` + `MVC 5`, `Entity Framework 6` and Domain Driven Design approach - makes it easy to extend, extremely flexible and essentially fun to work with ;-)
* **Website:** [http://www.smartstore.com/en/net](http://www.smartstore.com/en/net)
* **Forum:** [http://community.smartstore.com](http://community.smartstore.com)
* **Marketplace:** [http://community.smartstore.com/marketplace](http://community.smartstore.com/marketplace)
-* **Documentation:** [SmartStore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home)
+* **Translations:** [http://translate.smartstore.com/](http://translate.smartstore.com/)
+* **Documentation:** [Smartstore.NET Documentation in English](http://docs.smartstore.com/display/SMNET/SmartStore.NET+Documentation+Home)
+
+
+
## Highlights
@@ -25,22 +62,25 @@ The state-of-the-art architecture of SmartStore.NET - with `ASP.NET 4.5` + `MVC
* State of the art architecture thanks to `ASP.NET 4.5`, `ASP.NET MVC 5`, `Entity Framework 6` and Domain Driven Design
* Easy to extend and extremely flexible thanks to modular design
-* (NEW) A powerful theming engine lets you create themes & skins with minimum effort thanks to theme inheritance
-* Consistent and sophisticated use of modern components such as `jQuery`, `Twitter Bootstrap`, `LESS` & more in the front and back end.
-* Easy shop management thanks to modern and clean UI
-* Out of the box shop design on the highest level
-* Revolutionary Shopbar for the best shopping experience
+* Highly scalable thanks to full page caching and web farm support
+* A powerful theming engine lets you create themes & skins with minimum effort thanks to theme inheritance
* Point&Click Theme configuration
+* Highly professional search framework based on Lucene.NET, delivering ultra fast faceted search results
+* Consistent and sophisticated use of modern components such as `jQuery`, `Bootstrap 4`, `Sass` & more in the front and back end.
+* Easy shop management thanks to modern and clean UI
### Features
+* NEW: CMS Page Builder
+* NEW: CMS Menu Builder
* Multi-Store support
* Unlimited number of products and categories
* Product Bundles
* RESTful WebApi
-* Multi-language support
-* 100% Mobile optimized
-* Perfectly optimized for search engines
+* Multi-language and RTL support
+* Modern, clean, SEO-optimized and fully responsive Theme based on Bootstrap 4
+* Ultra fast search framework with faceted search support
+* Extremely scalable thanks to output caching, REDIS & Microsoft Azure support.
* Trusted Shops precertification
* 100% compliant with German jurisdiction
* Sales-, Customer- & Inventory-management
@@ -53,42 +93,47 @@ 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 V2.6.0 has been released on June 23, 2016. The highlights are:
-* Major improvements in Importer & Exporter: better field mapping, higher performance, bug fixes etc.
-* 'PayPal PLUS' payment plugin
-* 'paydirekt' payment plugin
-* 'Viveum' payment plugin
-* 'BeezUP' export provider
-* (Dev) Publishing SmartStore.Web from within Visual Studio now deploys the project correctly. No need to execute ClickToBuild.cmd anymore. Just publish directly to any target, including Azure.
-* Lots of bug fixes
+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.
+## Try it online
+
+We have set up a live online demo for you so you are able to test Smartstore.NET without local installation. Get a first impression and test all available features in the front- and in the backend. Please keep in mind that the backend demo is shared and other testers can modify data at the same time.
* [**Frontend**](http://frontend.smartstore.net/en) (User: demo, PWD: 1234)
* [**Backend**](http://backend.smartstore.net/en/login) (User: demo, PWD: 1234)
-##How to install
+
+
+## How to install
* Download the latest stable release from the download tab and unzip it to your web folder
* Setup a website in IIS and point the file directory to your unzipped folder
* Fire up your browser and follow the installation instructions
* Enjoy ;-)
-###System requirements
+NOTE: Smartstore.NET 3 requires [Visual C++ Redistributable für Visual Studio 2015](https://www.microsoft.com/en-US/download/details.aspx?id=52685) which is already pre-installed on most systems. If, nevertheless, it is missing on your web server, just download and execute the installer or ask your hosting provider to do that for you.
+
+### System requirements
* IIS 7+
-* ASP.NET 4.5
-* ASP.NET MVC 5
+* ASP.NET 4.5+
* MS SQL Server 2008 Express (or higher) OR MS SQL Server Compact 4
+* Visual C++ Redistributable für Visual Studio 2015 ([Download](https://www.microsoft.com/en-US/download/details.aspx?id=52685))
* Full Trust
+
## License
-SmartStore.NET is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
+Smartstore.NET Community Edition is released under the [GPLv3 license](http://www.gnu.org/licenses/gpl-3.0.txt).
diff --git a/SmartStoreNET.Tasks.Targets b/SmartStoreNET.Tasks.Targets
index d28563fc2e..080c78d497 100644
--- a/SmartStoreNET.Tasks.Targets
+++ b/SmartStoreNET.Tasks.Targets
@@ -19,7 +19,6 @@
$(RootFolder)\buildtasks
$(RootFolder)\artifacts
$(LibFolder)\sqlce
- $(LibFolder)\msvcp140
$(ArtifactsFolder)\Source
$(ArtifactsFolder)\MsDeploy
$(ArtifactsFolder)\Gallery
@@ -40,11 +39,11 @@
x86
$(BUILD_NUMBER)
- 3.0.0
+ 3.2.2
$(StageFolder)
.$(Version)
- .Web
+ .Community
@@ -91,19 +90,20 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -125,6 +125,7 @@
+
@@ -136,9 +137,9 @@
OutDir=$(StageFolder)\Administration\bin\;Configuration=$(Configuration)" />
-
-
-
+
+
+
@@ -153,12 +154,16 @@
-
-
-
+
-
+
+
+
+
+
+
+
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/assets/smnet3-devices-sm.png b/assets/smnet3-devices-sm.png
new file mode 100644
index 0000000000..2165a8ca6e
Binary files /dev/null and b/assets/smnet3-devices-sm.png differ
diff --git a/build.bat b/build.bat
index e8c6a8c006..94a6c72519 100644
--- a/build.bat
+++ b/build.bat
@@ -1,42 +1,45 @@
-FOR %%b in (
- "%VS140COMNTOOLS%..\..\VC\vcvarsall.bat"
- "%ProgramFiles(x86)%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"
- "%ProgramFiles%\Microsoft Visual Studio 14.0\VC\vcvarsall.bat"
-
- "%VS120COMNTOOLS%..\..\VC\vcvarsall.bat"
- "%ProgramFiles(x86)%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat"
- "%ProgramFiles%\Microsoft Visual Studio 12.0\VC\vcvarsall.bat"
-
- "%VS110COMNTOOLS%..\..\VC\vcvarsall.bat"
- "%ProgramFiles(x86)%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat"
- "%ProgramFiles%\Microsoft Visual Studio 11.0\VC\vcvarsall.bat"
- ) do (
- if exist %%b (
- call %%b x86
- goto findmsbuild
- )
-)
-
-echo "Unable to detect suitable environment. Build may not succeed."
-
-:findmsbuild
-
SETLOCAL ENABLEDELAYEDEXPANSION
-FOR %%p in (
- "%ProgramFiles(x86)%\MSBuild\14.0\Bin"
- "%ProgramFiles%\MSBuild\14.0\Bin"
- ) do (
- if exist %%p (
- if not defined MsBuildPath (
- SET "MsBuildPath=%%~p"
- goto build
+for /f "usebackq tokens=1* delims=: " %%i in (`lib\vswhere\vswhere -latest -requires Microsoft.Component.MSBuild`) do (
+ 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
)
)
)
-echo "Unable to detect suitable MsBuild version (14.0). Build may not succeed."
+FOR %%b in (
+ "%VS150COMNTOOLS%\vsvars32.bat"
+ "%VS140COMNTOOLS%\vsvars32.bat"
+ "%VS120COMNTOOLS%\vsvars32.bat"
+ "%VS110COMNTOOLS%\vsvars32.bat"
+ ) do (
+ if exist %%b (
+ echo "Using MSBuild from %%b"
+ call %%b
+ set msbuild="msbuild"
+ goto build
+ )
+)
+
+echo "Unable to detect suitable environment. Build may not succeed."
:build
-call "!MsBuildPath!\msbuild.exe" SmartStoreNET.proj /p:DebugSymbols=false /p:DebugType=None /P:SlnName=SmartStoreNET /maxcpucount %*
\ No newline at end of file
+cd /d %~dp0
+
+echo "Restoring NuGet packages"
+lib\nuget\nuget.exe restore "src\SmartStoreNET.sln"
+
+%msbuild% SmartStoreNET.proj /p:SlnName=SmartStoreNET /m /p:DebugSymbols=false /p:DebugType=None /maxcpucount %*
+
+:end
diff --git a/changelog.md b/changelog.md
index 3263b0a482..81357cca18 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,28 +1,556 @@
-# Release Notes
+# Release Notes
-## SmartStore.NET 3.0
+## SmartStore.NET 3.2.2
+
+### New Features
+* External category links: alternative links for categories in the main menu and in category listings.
+* Page Builder: multistore support for stories.
+* #1639 Shipping.ByTotal: Add option to calculate the shipping rate based on the net total instead of the gross total.
+* #1537 Implement "Ratenzahlung powered by PayPal" provider.
+* New Heidelpay payment plugin.
+* Shop-Connector: mapping of product SKUs that differ between client shops.
+* MegaMenu supports configuration of menu items
+
+
+### Improvements
+* Amazon Pay: PSD2. Strong customer authentication (SCA) upgrade.
+* Updated *MaxMind.GeoIP* library and country database
+* Removed HTML encoding for meta tags and document title.
+* #1662 Quantity Units: added field for name pluralization.
+* (SEO) combined SSL and canonical host 301 redirections to prevent successive redirects.
+* (SEO) Activated GZIP compression for all dynamic and static responses.
+* Set max. file upload size to 2 GB (globally)
+* (Dev) Added a sample block for the *Page Builder* in DevTools plugin.
+* Various other minor improvements.
+* PayPal PLUS: paying by instalments can be selected in payment wall.
+* Payone: send more information to API (e.g. shipping address) to stay as frictionless as possible for PSD2.
+* PageBuilder: Added keyboard shortcuts
+* #1582 Added structured data to reviews
+* BMEcat: added support for import of specification attributes
+
+### Bugfixes
+* Fixed wrong XML sitemap index numbering.
+* #1646 Setting ManufacturerItemsToDisplayOnHomepage to 1 will not show any manufacturer.
+* #1647 All manufacturers page: link of last character missing in alphabetical letter list.
+* #1664 RTL: CMS menu issues in backend.
+* #1677 Page Builder: moving and resizing blocks via drag & drop didn't work correctly in RTL.
+* Do not adjust inventory twice when an order is cancelled and deleted.
+* #1500 Fallback to fixed VAT rate instead of rate by region for countries that are not subject to vat.
+* Fixed "The entity wrapper stored in the proxy does not reference the same proxy" when copying an export profile.
+* Various other minor fixes.
+* BMEcat: set TaxCategoryId & MainPictureId for newly imported products
+
+
+
+## SmartStore.NET 3.2.1
+
+### Improvements
+* Implemented file range support for CachedFileResult
+* Sitemap should only include products visible for current customer, which is the search engine system customer
+* Fixed uniform spelling for query string parameter "returnUrl"
+* Fixed search engine issue (itemtype, "offers" required) on product detail page
+* Summernote: solved html formatting issues thanks to "Beautifier" library
+* Media middleware: simplified ETag generation & handling
+
+### Bugfixes
+
+* Mega Menu: Product rotator wasn't working
+* Tiny Image: Fixed periodic IOException
+* URL Rewriter: performance optimization
+* Fixed more menu for main catalog navigation
+* Fixed DateTime nor expressed as UTC in AzureFile & LocalFile
+* Content Slider: Fixed slide display bug on surface and other touch devices
+* Content Slider: Randomize slides > Value was stored in database but toggle button has always shown "off"
+* Page Builder: Fixed slider bug for list blocks (brand, category, products) in Firefox
+* Page Builder: fixes blurry background images in IOS
+* Output Cache: action widgets didn't render when they were part of a widgetzone within a donut child action
+* Multishop resolution failed under certain circumstances
+* Fixed menu issue "collection was modified, enumeration operation may not execute"
+* Fixed "The parameters dictionary contains a null entry for parameter 'catId' of non-nullable type 'System.Int32'"
+* Fixed discount coupon sometimes wasn't applied
+* Fixed "Value cannot be null. Parameter name: key" in order list
+* Fixed topic link resolving by system name in multistore environment
+* Async event consumers were not guaranteed to be awaited
+
+
+## SmartStore.NET 3.2
+
+### Highlights
+* (NEW) **CMS Page Builder**: Create fascinating content that will boost your sales. No coding is required thanks to a powerful WYSIWYG editor which utilizes the revolutionary *CSS Grid* system (commercial plugin)
+* (NEW) **CMS Menu Builder**: Visual manager for all sorts of menus. Change existing menus or create your own and place them anywhere you want.
+* (Perf) **Faster MegaSearch** thanks to Lucene.NET 4.8
+* (Perf) **Huge performance increase** in discount resolution and calculation.
+
+### New Features
+* **EmailReminder**:
+ * Reminders for open shopping carts
+ * Reminders for product reviews
+ * Reminders to visit the shop after a long absence
+* **Scheduling**
+ * Task execution history
+ * Web farms: tasks can run decidedly on each machine of a web farm
+ * #1144 MegaSearch: multi server search index
+* **Topics & Pages**
+ * Access control lists
+ * Grid paging & filtering
+ * New properties: added **IsPublished**, **Short Title** (link text) and **Intro** (teaser) properties.
+ * #1542: added options to set body CSS class and Html Id.
+* **MegaSearch**:
+ * Supports searching for forum posts.
+ * #1172 Option to display related search terms on search page.
+ * Command to continue a previously aborted search index rebuild.
+* **Import/export of**:
+ * Product tags
+ * Tier prices
+ * Product attribute options
+ * Product attribute combinations (update only)
+* **Forum**:
+ * Added option to display a captcha on forum pages when creating or replying to a topic.
+ * #417 Restrict forum groups to specific customer roles.
+ * Added published property to forum topic and post.
+ * Added voting for forum posts.
+ * Several performance improvements.
+* **BeezUp**:
+ * #1459 Add option to only submit one category name per product
+ * Allow to specify export categories per product
+* **Santander**:
+ * Added payment method for instalment purchase.
+ * Added financing calculator.
+* Updated icon library to **Font Awesome 5 Free** with the possibility to activate pro version in the frontend.
+* New storefront catalog options: **ShowSubCategoriesInSubPages**, **ShowDescriptionInSubPages** & **IncludeFeaturedProductsInSubPages** (Subpage = List index > 1 or any active filter).
+* New security option: Use **invisible reCAPTCHA**
+* Wallet: Allow customer to choose whether refund should be submitted to his wallet.
+* Added option to display preview pictures in product lists
+* Added option to add multiple file versions to product download section
+* Added options for alternating price display (in badges)
+* Customer avatar: Letter with colored background if no avatar image was uploaded.
+* Viveum: Supports payment via "Virtual Account Brands" (e.g. PayPal).
+* Added options for alternating price display (in badges).
+* #1515 Poll: Add result tab with a list of answers and customers for a poll
+* BMEcat: Added export and import of product tags.
+* Web-API: Added endpoints for ProductSpecificationAttribute.
+* Trusted Shops: Added new Trustbadge display for mobile devices
+* (Dev) Added *sm:EfCommandTimeout* setting to web.config (was 30 sec. fixed, now configurable)
+
+### Improvements
+* (Perf) Significantly increased query performance for products with a lot of category assignments (> 10).
+* (Perf) Increased app startup speed by up to 20%.
+* (Perf) Ultra-fast file-based XML sitemap generation for extremely large catalogs (> 1M)
+ * At least 10x faster
+ * Generated files are saved on the hard disk now: a rebuild after an app restart is no longer necessary.
+ * No exclusive locks during rebuilds anymore: if an (outdated) file already exists, it is returned instantly.
+* (Dev) Enhanced pub/sub: new `IConsumer` marker interface for classes that contain one or more event message handlers. The generic `IConsumer` implements `IConsumer` but has been marked as `Obsolete`.
+* Better protection against XSS attacks
+* Updated to Bootstrap version 4.1.3
+* **Debitoor**:
+ * Partially update customer instead of full update to avoid all fields being overwritten.
+ * #1540 Place company name in front of customer name (according to address format of the particular country).
+ * #1565 Add a button to manually create a draft invoice.
+* #1479 Show in messages the delivery time at the time of purchase
+* #1184 Sort current shopping carts & current wishlists by `ShoppingCartItem.CreatedOn`.
+* #1106 BMECat: import & export support for product keywords
+* #1499 Added hint to forms indicating that fields with an asterisk (*) are required
+* Added filter for newsletter subscriber export by working language
+* Refactored download section
+* Enhanced EntityPicker to pick from customers, manufacturers & categories
+* #1510 Breadcrumb of an associated product should include the grouped product if it has no assigned categories.
+* OpenTrans: added customer number to parties
+* Do not filter cookie using resources if cookie usage has not yet been consented to.
+* #1563 QueuedMessagesClearTask: add a setting for the age of the mails to be deleted.
+* #1569 Added a setting to show login note if no prices are displayed due to customer group permissions.
+* PayPal PLUS: Up to 10 more third party payment methods are allowed by PayPal now.
+* #1560 Tell-a-Friend and anonymous user: render alert box and tell guest to login to use this function
+* #1571 Compare products now shows all specification attribute options
+* #1539 Signing in is now allowed with e-mail and username
+* Trusted Shops: Trustbadge won't be displayed in Popups & Iframes anymore
+* #1461 Admin Grid: filter dialog will be displayed entirely even when grid has no data to display
+* Lots of minor enhancements for both frontend and admin theme
+* Many other minor improvements
+
+### Bugfixes
+* In a multi-store environment, multiple topics with the same system name can now be resolved reliably.
+* **GMC**:
+ * Export the product images if no attribute images are defined
+ * Do not export the first image twice for additional images
+ * Export image URL of full size image (not default size) for additional images
+ * Custom labels are now being exported
+* Media middleware: 0-byte files should be treated as missing.
+* Megamenu alpha/omega blends do now toggle correctly on touch devices
+* Summernote HTML editor exceeds parent container width when CodeMirror is activated
+* Only display a zero search hits warning if at least one filter is activated
+* #1436 Do not display delivery time in customer order completed messages
+* "ArgumentNullException: The value must not be NULL" if a topic is password protected
+* Tax by region: Fixes after inserting a tax rate country column shows "Unavailable"
+* #1014 Switching to default language keeps specific URL alias of current page
+* Shipping by total: When inserting a record the country now will be saved
+* #1460 Editing of the customer title was missing on customer and address pages in the backend
+* #1447 Checkout button payment methods (Amazon, PayPal Express) won't work in conjunction with mandatory checkout attributes
+* Fixed Autofac circularity error
+* When creating a topic, the widget zone input has shown System.String[]
+* Switching the language always redirected to the home page if SEO friendly URLs was deactivated.
+* File upload of a checkout attribute was not stored on cart page.
+* Redirecting within checkout may have displayed an incorrect URL in the browser.
+* Server cannot modify cookies after HTTP headers have been sent.
+* Wrong base price on product and cart page when a special price is active.
+* In a multi-store, message templates may have loaded the wrong disclaimer and conditions-of-use text.
+* NullReferenceException in manufacturer list when there is no manufacturer.
+* Wrong order of featured products on category page.
+* #1504 Cart item price calculation was wrong if attribute combinations with text types were involved.
+* #1485 Dropdown list for product sorting didn't not work with Internet Explorer 11.
+* #1468 Twitter authentication wasn't working anymore.
+* Newsletter subscription didn't work when customer privacy setting DisplayGdprConsentOnForms was turned off
+* Fixed social media image detection
+* Fixed redirection of bots when several languages were active
+* Region cannot be selected in checkout when entering a billing or shipping address
+* Fixed invalid conversion of "System.Int32" to "SmartStore.Core.Domain.Tax.VatNumberStatus" when placing an order
+* MegaMenu: Improved item rendering for third tier elements
+* Product display order on category and manufacturer pages was sometimes wrong when using linq search.
+* Debitoor: Wrong invoice total if a subtotal discount has been applied.
+* Import: Fixes invalid conversion "System.Double" to "SmartStore.Core.Domain.Catalog.QuantityControlType".
+* Topics: Fixes "Cannot insert duplicate key row in object 'dbo.UrlRecord' with unique index 'IX_UrlRecord_Slug'".
+* #1566 Santander: eliminate the 1 cent rounding difference at amountTotalNet.
+* Fixed redirection to the homepage for pages which are loaded while the application is restarted.
+* #1570 Filter option "Only deactivated customers" filters deleted instead of deactivated customers.
+* #1475 select boxes must be wrapped on mobile devices if data-select-url is set
+* Fixed the redirection to the homepage for pages which were loaded while the application was restarted
+* Fixes product feeds expect a different base price formatting.
+* #1369 Shopping cart shows "Discount code applied", although it is not applied due to a lower tier price.
+* Many other fixes...
+
+
+## SmartStore.NET 3.1.5
+
+### Highlights
+* Compliance with EU-GDPR requirements
+* Search engine friendly topic URLs
+* "Honeypot" bot detection for registration and contact forms.
+
+### New Features
+* #1429 Search engine friendly topic URLs
+* Implemented cookie consent according to EU-GDPR
+* Added checkboxes for data processing consent in all relevant forms
+* Implemented "Honeypot" bot detection for registration and contact forms.
+* Trusted Shops: Added consent checkbox to confirm order page for submission of customer email address to Trusted Shops if review widget is active
+* #1226 Shop-Connector: Added exchange of tier prices and delivery times
+* #1439 Debitoor: Option whether to display the payment method and SKU on invoices
+
+### Improvements
+* Added double opt-in feature for newsletter subscriptions during checkout (confirm order)
+* Allow forward slash in product tag URL slug
+* Theming: throttle AJAX cart refresh after spin up/down click
+* Moved StoreLastIpAddress & DisplayPrivacyAgreementOnContactUs from customer settings to privacy settings tab
+* #1450 Show the regular price only if it's higher than the final price
+* #1450 Do not ignore discounts with a negative amount
+* (Soft) deleted customers can be edited now
+* Customer IP addresses will be anonymized on (soft) deletion
+* Set catalogsettings.showsharebutton to false as its not compliant with GDPR
+* Made form fields for first & last name in customer registration optional
+* Implemented settings to make form fields for first & last name required again
+* Made form field for full name in contact us & product request optional
+* Implemented settings to make form field for full name in contact us & product request required again
+* #1453 Import: Use [IGNORE] to ignore a field value on record level
+* #1455 More detail on packing slip when bundled item
+* Display category alias as badge in grids and dropdowns
+
+### Bugfixes
+* Migration: take all same-named message templates into account
+* Messaging: OrderPlaced e-mail templates show main product image even when an attribute combination with a custom image has been selected
+* Theming: fix broken product review voting
+* Theming: added missing bottom space to .html-editor-content
+* Theming: Language switcher is not displayed if no currency options are available
+* No bundle item thumbnail displayed in bundle summary if item is not individually visible
+* Tracking number in shipment details was not saved
+* Assigning or removing product tags did not invalidate model cache
+* Reward points weren't displayed in message templates
+* Dashboard: link for not yet shipped orders loads list with all orders
+* Topic search button had no effect
+* #1442 Message factory should not throw an exception if a template has been deactivated
+* Fixes script error "$(...).tab is not a function" on product detail page
+* Title attribute for the product name in product lists was sometimes truncated
+* Relativizing font sizes should cast to double, not int
+* Fixes category list on product edit page shows empty category name
+* #1438 Debitoor: The country is displayed twice
+* MegaSearch: Fixes indexing ignores DeliveryTimeIdForEmptyStock setting
+* Web API: Fixes "No NavigationLink factory was found for the navigation property 'WalletHistory'"
+* #1449 IgnoreCharges of shipping methods is not working if a localized name is specified
+* Fixes "The object does not support the property or method 'startsWith'" on product edit page.
+* Wallet: Fixes "Child actions are not allowed to perform redirect actions" when there are cart warnings
+* Fixes the delivery time in the order notifications may differ from delivery time on the product detail page
+
+
+## SmartStore.NET 3.1.0
+
+### Highlights
+* **Wallet**: Enables full or partial order payment via credit account. Includes REST-Api. (commercial plugin)
+* **[Liquid](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers) template engine**: very flexible templating for e-mails and campaigns with autocompletion and syntax highlighting.
+* **Cash Rounding**: define money rounding rules on currency and payment method level.
+* **Modern, responsive backend**: migrated backend to Bootstrap 4, overhauled and improved the user interface.
+* **Enhanced MegaMenu**: virtual dropdowns for surplus top-level categories and brands (commercial plugin exclusively bundled with Pro Edition).
+* **RTL**: comprehensive RTL (Right-to-left) and bidi(rectional) support.
+* **Amazon Pay**:
+ * Supports merchants registered in the USA and Japan
+ * External authentication via *Login with Amazon* button in shop frontend
+ * Several improvements through the new *Login and pay with Amazon* services
+* (Perf) **Faster image processing**: new processing and caching strategy! Thumbnails are not created synchronously during the main request anymore, instead a new middleware route defers processing until an image is requested by any client.
+* **TinyImage**: scores ultra-high image compression rates (up to 80 %!) and enables WebP support (commercial plugin exclusively bundled with Premium Edition).
+* **UrlRewriter**: define URL redirection rules in the backend using *mod_rewrite* notation. (commercial plugin)
+* **Address formatting** templates by country
+* **Language packs**: downloader & auto-importer for packages available online.
+
+### Breaking changes
+* Message template customizations are lost due to the new template engine. You have to customize the templates again. No automatic migration, sorry :-(
+* Amazon Pay: The plugin has been changed to new *Login and pay with Amazon* services. The client ID has been added, which has to be created in Amazon Seller Central and saved in the payment method configuration.
+* (Dev) Calls to cache methods `Keys()` and `RemoveByPattern()` require glob chars to be present now (supported glob-styles see [https://redis.io/commands/keys](https://redis.io/commands/keys)). Previously these methods appended `*` to the passed pattern, which made pattern matching rather unflexible.
+* (Dev) Hook framework now passes `IHookedEntity` interface instead of `HookedEntity` class
+* (Dev) Completely removed all `EntityInserted`, `EntityUpdated` and `EntityDeleted` legacy events. We were using DbSaveHooks anyway, which provides a much more powerful and way faster pub-sub mechanism for database operations.
+
+### New Features
+* 1203 MegaMenu shrinker and *Brands* virtual menu item
+* [Summernote](https://summernote.org/) is now the primary HTML editor
+* #431 Added option to randomize the display order for slides on each request
+* #1258 Add option to filter shipping and payment methods by a specific customer role
+* #1247 Allow to import non system customer roles in customer import
+* #1117 Added an option to display a dropdown menu for manufacturers
+* #1203 Added an option to define a maximum number of elements in the main menu for the first hierarchy of the catalog navigation
+* GMC: column chooser for edit grid
+* #1100 Customer can register in frontend via "Login with Amazon" button
+* **Web API**:
+ * #1292 Added endpoint to get order in PDF format
+ * Added endpoint to complete an order
+ * #1364 Added endpoints for MeasureWeight and MeasureDimension
+* Added options to include option names of specification and product attributes in the search index
+* #441 added option to specify that additional shipping surcharges are considered only once.
+* #1295 Sales tracking (tracking pixel) for Billiger.de
+* XML and CSV export of shopping cart and wishlist items
+* #1363 Make storing of IP addresses optional
+* #729 Option for automatic order amount capturing when the shipping status changed to "shipped"
+* (Dev) ILocalizationFileResolver: responsible for finding localization files for client scripts
+* #998 GMC: Find a way to map attribute combination values to feed export values
+* Added Instagram icon to social media icons in footer
-### Breaking change
-* Removed Web API plugin from open source core
+### Improvements
+* Target .NET Framework changed: 4.5.2 > 4.6.1.
+* Lower memory consumption
+* #649 Media FileSystem provider: segmenting files in subfolders to increase IO perf with huge amount of files
+* #1141 Cleaner backend order list. Added more infos like payment method.
+* OuputCache: Simple product changes that affect visibility now correctly invalidate all assigned category and manufacturer pages
+* * OuputCache: When MegaSearch is active, invalidation occurs only during indexing and not ad-hoc anymore.
+* #1248 New payment integration guidelines for Sofort\Klarna
+* TwitterAuth: better error handling and enhanced admin instruction
+* #1181 Debitoor: Add option to display shipping address on invoices
+* Moved RoundPricesDuringCalculation setting to currency entity
+* #1100 Use new "Login with Amazon" services to initialize an Amazon payment
+* #1285 Copy product: Add option to add more than one copy
+* (Perf) Many improvements in hooking framework
+* #1294 Swiss PostFinance: External payment page too small on mobile devices. Added setting for mobile device template URL, pre-configured with PostFinance template.
+* #1143 Make shipping methods suitable for multi-stores
+* #1320 Image import: Find out the content type of image URLs by response header rather than file extension (which is sometimes missing)
+* #1219 Recently viewed products list should respect setting to hide manufacturer names
+* Import and export product quantity step
+* Add bundle item information to order messages
+* #1031 Enable offline payment methods to have brand icons
+* DevTools Plugin: Added example for cached output invalidation
+
+### Bugfixes
+* #1268 Data importer always inserts new pictures and does not detect equal pictures while importing
+* OutputCache computes ambiguous cache keys for blog pages
+* #1142 Customer import creates role multiple times
+* #1244 Variant query model binder cannot handle types text and datepicker
+* #1273 Attribute formatter should consider setting CatalogSettings.ShowVariantCombinationPriceAdjustment
+* Product entity picker should use the wildcard search to find products
+* Hook framework should run hooks with `ImportantAttribute` when hooking was disabled per scope
+* #1297 Web API: Parsing the timestamp may fail due to the different accuracy of the milliseconds
+* Debitoor: VAT amount could be transmitted as miscellaneous for deliveries abroad.
+* Prices with discounts limited to categories and customer groups were shown to all users in product lists
+* #1330 MegaSearch: Missing variant facets if the variant value is not unique
+* Back-in-stock subscription form was already submitted when opening the popup dialog
+* Associated products of a grouped product were displayed in the wrong order
+* Payment-Filter: Fixed "The cast to value type 'System.Decimal' failed because the materialized value is null"
+* The tax value per tax rate was not updated when adding\removing a product to\from the order.
+* The option to send manually was ignored when sending e-mails
+* #528 LimitedToStores is required on payment provider rather than plugin level
+* #1318 Disabled preselected attribute combination permanently hides the shopping cart button, even if another combination is selected.
+* Copy product: Fixes "Cannot insert duplicate key row in object dbo.UrlRecord with unique index IX_UrlRecord_Slug"
+* Fixed export publishing via email
+
+## SmartStore.NET 3.0.3
+### Bugfixes
+* #1232 Scheduled tasks are being 'randomly' set inactive
+* #1245 Existing product does not get updated during import under certain circumstances
+* #1207 No low stock activity when bulk editing the product quantity
+* #1208 Missing inventory tab when switching from bundle to simple product
+* #1210 Clear asset cache when a theme setting has changed
+* Fixed name of deleted manufacturers displayed in product lists
+* Fixed non-system category export profile always exports categories of current store
+* Fixed picture assignment for variant attribute combinations of product copies which weren't assigned correctly
+* #1218 CopyProductService does not copy picture of ProductVariantAttributeValue
+* Fixed hidden checkout button when there are two or more shopping cart items and one gets removed from basket during checkout
+* Fixed physical gift cards cannot be moved to cart
+* Fixed tax rate wasn't properly formatted on product detail page
+* #1228 Text entered in frontend for attributes or checkout attributes are truncated by comma
+* Fixed shipping computation method ignoring deactivated PricesIncludeTax setting
+* **Debitoor**: Fixed missing tax rates on Debitoor invoice for net prices
+* #1224 Notifier wasn't working in plugin controllers
+* #1205 Server cannot append header after
+*
+* headers have been sent
+* #1154 Left offcanvas navigation does not open when in checkout progress
+* #1212 Export: FTP publishing should consider directory structure
+* #1253 Product PDF exporter only exports one picture and ignores the picture number profile setting
+* Configuration of some plugins not saved in Firefox browser
+
+### Improvements
+* Apply img-fluid class to all images within html editor content
+* System name in privacy info topic URL should be lowercase
+* If no checkout attribute is required checking out will be possible
+* **OpenTrans**: optimizations for Lexware import
+* #496 show bonus points in ordersummary on first checkout page
+* Discounts of plugins weren't displayed in product lists
### New Features
-* Output Cache with "donut hole caching" (commercial plugin)
-* REDIS providers for Business Cache, Output Cache and Session State (commercial plugin)
-* Microsoft AZURE provider for media storage (commercial plugin)
+* #1076 Add support for percental tier price adjustments
+* **Trusted Shops**: added review import
+* (Dev) SSL redirection on localhost will be bypassed
+* #783 Added option for "Email a friend" which disables the form field "Your email address" so it can't be changed
+
+## SmartStore.NET 3.0.2
+### Breaking changes
+* MegaSearch: The index must be re-created because of changes to the price filter.
+
+### New Features
+* New setting indicates whether to include or exclude unavailable products in search results by default.
+* (Dev) New Asset Cache
+
+### Improvements
+* PayPal Standard: New settings "UsePayPalAddress" and "IsShippingAddressRequired" to avoid payment rejection due to address validation.
+* More widget zones for the footer
+* #1177 Implement new setting for product search sorting
+* More fault tolerant task scheduler
+
+### Bugfixes
+* Fixed memory leak in MegaSearch plugin, which caused HTTP 503
+* RSS: Item URLs in feeds have ignored SSL settings
+* Fixed "The view 'ProfileImportResult' or its master was not found"
+* #1187 Search: Do not hide multi-selectable filters of the same group if there are no hits
+* Fixed System.ArgumentNullException in ProductVariantQueryFactory
+* PayPal PLUS: Fixed HTTP 401 "Unauthorized" when calling PatchShipping
+* #1189 MegaSearch: Boosts are ignored in prefix and wildcard queries by default
+* MegaSearch: Localized labels of filters were never displayed
+* #1195 Exporter: don't send an email if no email account has been selected
+* Product lists sometimes show the wrong delivery time
+* #1192 Lucene indexing
+*
+* decreases the longer it takes
+* #1198 MegaSearch: never sort numeric range by label, always by value
+* Filter for attributes were always sorted by hit count
+* #1200 PayPal PLUS: Invalid request if the order amount is zero
+* Fixed null reference exception when copying attribute option set with image(s) (file system storage only)
+* Product price sometimes was wrong when entering a numeric value in an attribute text box
+* Added missing code for customer privacy agreement
+* Clicking the login link wasn't working correctly in offcanvas my-account menu
+* #1158 Currency and language selectors weren't working in OffCanvas menu
+* Fixed "Server cannot append header after HTTP headers have been sent"
+
+## SmartStore.NET 3.0.1
+### Improvements
+* (Perf) Much faster application startup
+* Microsoft Visual C++ 2015 Redistributable no longer required to be installed
+* **BeezUP**:
+ * Exports up to 9 product images
+ * Export stock quantity when stock is managed by attributes
+ * Export parent child relation data when exporting attribute combinations as products
+ * Flatten and append attribute data when exporting attribute combinations as products
+* Instant search should search manufacturer/brand name
+* HTTP 301 redirects for legacy media URLs
+* (Dev) New 'ApplicationStarted' event
+* (Dev) Enabled C# 6 features in plugin views
+* (GMC) Added missing database indexes.
+* (WebApi) Added endpoints for blog post and blog comment
+* Added more sortable columns to backend product grids
+* #1160 Append suffix 'Picture (1...n)' to thumbnail's alt-attribute on product detail pages
+* (Perf) Faster catalog indexing thanks to new database indexes
+* (Perf) Faster dynamic view compilation thanks to *Roslyn* compiler
+* Added the widget zone 'productdetails_pictures_bottom'
+* 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
+* The file manager did not work correctly under a virtual folder
+* Item row in list style product list should not wrap on small devices
+* Item row in list style product list squashed on Android system browser
+* Linq search: Filter for manufacturers and categories were not limited by store
+* (GMC) Fixes duplicate occurrence of application path in absolute product URL
+* Products with stock managed by attributes sometimes not displayed in category lists
+* Fixed System.MissingMethodException for SmartStore.Core.Search.Facets.FacetGroup
+* Fixed uploaded image paths in HTML fields (Media/Uploaded > Media//Uploaded)
+* (MegaSearch) Product category order, product manufacturer order and product order were not recognized (sorting)
+* Standard search: Fixed sorting of recently added products
+* Alias for search filters couldn't be set if there's only one language
+* #1168 Cart: MinOrderAmount sometimes ignored
+* Moving products from editable wishlist to cart was out of function
+* Fixed several issues with parallelly executed data export tasks
+* Fixed several issues with *PayPal* payment providers
+* Facebook login out of function due to Facebook API changes (always returns "Unknown error")
+* Fixed 'no picture available' watermark on some product pictures
+* #1153 Removing already applied gift card causes error
+* (Theming) *Drift* zoom uses static zoomFactor. Made it dynamic.
+* (Theming) IE11: simple menu dropdowns weren't positioned correctly
+* Removed "trust" element from web.config (causes problems on some shared hosting spaces)
+* Fixed ThumbZoomer unproportional rescale issue in product grid
+* #1134 JavaScript error (IE 11 in VS debug mode) after removing the last item in shopping cart
+* (Theming) Dozens of layout fs for ixemobile devices (especially iOS and Android native browser)
+* Better error logging for TaskScheduler
+* Fixed several queries which caused problems with SQL CE
+* Fixed missing CurrencyCode exception in product exports
+* #1179 Error while downloading a downloadable product
+* Mega Menu: If a dropdown contains two level hierarchy categories only, they are not wrapped to the next column
+* Hide sort dropdown when option is off
+* HTTP 301 redirects for legacy media URLs
+* Added missing resources of broken migration 'AddressEnhancement' again
+* Checkout: Title and salutation were missing when entering new addresses
+* #1163 ContentSlider: pictures won't be displayed when placed on top or bottom on IOS
+
+## SmartStore.NET 3.0
+
+### Highlights
+* **Flex**: New mobile-first responsive Theme based on Bootstrap 4
+* **Mega Search**: highly professional search framework based on Lucene.NET (commercial plugin exclusively bundled with Pro Edition)
+ * Ultra fast search results, even with millions of items
+ * Faceted search
+ * Synonyms
+ * Compound word splitting
+* **Mega Menu**: highly customizable catalog menu widgets (commercial plugin exclusively bundled with Pro Edition)
+* **Content Slider**: creates eye-catching content to boost sales (commercial plugin exclusively bundled with Pro Edition)
+* **Output Cache** with "donut hole caching" for maximum speed and scalability (commercial plugin exclusively bundled with Premium Edition)
+* **Microsoft AZURE** provider for media storage (commercial plugin exclusively bundled with Premium Edition)
+* Web Farms: **REDIS** providers for Business Cache, Output Cache and Session State (commercial plugin exclusively bundled with Enterprise Edition)
+* Product variant **option sets**
+* New product specification attribute type: **numeric range**
+* Image support for variant attributes
+
+### Breaking changes
+* Removed old **Content Slider** module from open source core
+* Removed **Web API** plugin from open source core (now exclusively bundled with Premium Edition)
+
+
+### Other New Features
* Message Bus for inter-process messaging between servers (commercial plugin)
* Configurable media storage path for web farms
* (Dev) log4net integration
* (Dev) proper plugin view debugging
-* (Dev) changes to static plugin files (css, js, etc.) immediately apply to running project
+* (Dev) detect changes to static plugin files (css, js, etc.) and immediately apply them to running project
* (Dev) Theming: plugins now support implicitly imported SASS files (Content/[public|admin].scss)
* XML Sitemap optimized for very large catalogs:
* Partitions for very large sitemaps (> 50.000 nodes or > 10 MB)
* Generated in a background task. No instant invalidation anymore.
-* Added option to skip shipping method selection in checkout process when only one shipping method is active
+* Added option to skip shipping method selection in checkout process if only one shipping method is active
* Added options to capture salutation and title in addresses and customer info
* Added projection to control the export of individually visible associated products
-* #966 Implement new tax calculation logic for shipping and payment fees (Calculate with rate of highest cart amount)
-* #922 New option whether to include the weight of free shipping products in shipping by weight calculation
+* #966 Implement new tax calculation logic for shipping and payment fees (calculate with rate of highest cart amount)
+* #922 New option to specify whether to include the weight of free shipping products in shipping by weight calculation
* #724 Allow discounts to be applied to manufacturers
* Option to display manufacturers sorted in alphabetical order
* #1093 New product properties required for selling abroad
@@ -35,9 +563,11 @@
* #1064 Deleting all product categories/manufacturers per product in one go
* #1063 Adding product category/manufacturer ignores any other property like DisplayOrder
* Added endpoint "Infos" for order and order item entity for additional information like aggregated data.
+ * Swagger integration
* Added setting to specify whether the product manufacturer should be displayed in product detail
* #271 Implemented support for attribute images/icons
* #330 Implement 'attribute option sets'
+* Two themes Flex Black and Flex Blue which are derived from Flex theme
### Improvements
@@ -45,7 +575,6 @@
* Added order message token for accepting third party email handover
* ECB currency exchange rate provider now cross calculates rates based on euro rates
* BeezUP: Exports the product weight
-* Payone: Replace client API by Payone iFrame solution. Allows credit card payment compliant with PCI DSS, SAQ A.
* Updated Elmar shop info XML from version 1.1 to 2.0
* (Perf) Application start faster by ~20%
* (Perf) Lower memory consumption
@@ -60,27 +589,39 @@
* Order XML export includes more customer data like title and salutation
* #1080 Do not resolve Order.OrderURLForCustomer token for guest customers
* #1051 Display EAN in PDF packaging slip
-* #806 Can't create a user in the admin section without "Manage Customer Roles" permission
+* #806 User couldn't be created in the admin section without "Manage Customer Roles" permission
* #491 Implemented option to determine a default delivery time
* #1078 Implemented options for "Add to Cart" quantity input field on product level. Quantity control can now be hidden and can be configured to be increased and decreased in steps.
* #696 Added textual resources for meta title and description for blog per month and by tag pages and enriched them with corresponding month plus year respectivly tag name
* #1025 Added token for customer number to MessageTokenProvider
* #1016 Implemented choosing of MessageTokens for newsletter campaigns with dropdown menu
+* #1107 OpenTrans: Added option to exclude long description of products from export
+* BMEcat: Implemented download of pictures according to MimeRoot-Info
+* Payone:
+ * Replace client API by Payone iFrame solution. Allows credit card payment compliant with PCI DSS, SAQ A.
+ * #1123 Add order note with payment info for prepayment and invoice payment.
### Bugfixes
-* Currency wasn't displayed at shipping estimation
-* SKU, EAN, MPN of last attribute combination was exported for all combinations
-* GMC: Id should be unique when exporting attribute combinations as products
-* GMC: Attribute price adjustments were ignored when exporting attribute combinations as products
-* GMC: Associated products that are not individually visible are not exported anymore. GMC rejects them because the frontend redirects to the grouped product.
-* #999 Export: Projected customer id ignored during price calculation
-* Awarded reward points for a placed order sometimes wrong calculated
-* PayPal PLUS: A changed shipping address/costs was not transmitted to PayPal
+* Currency wasn't displayed for shipping estimation
+* SKU, EAN, MPN of last attribute combination were exported for all combinations
+* GMC:
+ * Id should be unique when exporting attribute combinations as products
+ * Attribute price adjustments were ignored when exporting attribute combinations as products
+ * Associated products that are not individually visible are not exported anymore. GMC rejects them because the frontend redirects to the grouped product.
+* Export:
+ * #999 Projected customer id was ignored during price calculation
+ * #1104 Language projection was ignored when creating product details URL
+ * #1030: Orders didn't include data of attribute combinations
+ * Orders didn't include shipping address
+* Awarded reward points for a placed order were occasionally calculated wrong
+* PayPal PLUS:
+ * Changed shipping address/costs were not transmitted to PayPal
+ * Skip payment if cart total is zero
+ * Do not display payment wall if method is filtered
+* PayPal Express: Fixed net price issue.
* Bundle item cannot be deleted if it's in a shopping cart
* Fixed SSL issue for news items in RSS feed
-* #1030: Order export: does not export the data of the attribute combination
-* Order export does not export shipping address
* Filter shows wrong number of products if "Include products from subcategories" is activated
* Guest user cannot be deleted if he has a private message
* #1029 Biz-Importer: Imports fixed tax rates as zero percentage
@@ -88,6 +629,8 @@
* #1071 NewsLetterSubscription.ActivationUrl not working if the store if SSL secured
* #1086 Gift cards can be earned in one store and applied in another
* Whishlist: Products with minimum order amount greater then 1 can be added to the whishlist without any warning
+* #1102: Discounts not transmitted when the total amount is zero
+* #1101 Clickatell: Sending SMS not working anymore
## SmartStore.NET 2.6
@@ -219,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
@@ -839,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 a56aac44b3..17bf4e2ad9 100644
Binary files a/lib/SmartStore.Licensing/SmartStore.Licensing.dll and b/lib/SmartStore.Licensing/SmartStore.Licensing.dll differ
diff --git a/lib/msvcp140/amd64/msvcp140.dll b/lib/msvcp140/amd64/msvcp140.dll
deleted file mode 100644
index c554731082..0000000000
Binary files a/lib/msvcp140/amd64/msvcp140.dll and /dev/null differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-console-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-console-l1-1-0.dll
new file mode 100644
index 0000000000..b469ccbd58
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-console-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-datetime-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-datetime-l1-1-0.dll
new file mode 100644
index 0000000000..d0cdea4bb6
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-datetime-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-debug-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-debug-l1-1-0.dll
new file mode 100644
index 0000000000..11283e3db3
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-debug-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-errorhandling-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-errorhandling-l1-1-0.dll
new file mode 100644
index 0000000000..1069800f35
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-errorhandling-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-file-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-file-l1-1-0.dll
new file mode 100644
index 0000000000..b608d0c7aa
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-file-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-file-l1-2-0.dll b/lib/msvcp140/x64/api-ms-win-core-file-l1-2-0.dll
new file mode 100644
index 0000000000..620539cb6e
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-file-l1-2-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-file-l2-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-file-l2-1-0.dll
new file mode 100644
index 0000000000..16f3d88c6b
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-file-l2-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-handle-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-handle-l1-1-0.dll
new file mode 100644
index 0000000000..cbd019ace3
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-handle-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-heap-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-heap-l1-1-0.dll
new file mode 100644
index 0000000000..963f042adc
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-heap-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-interlocked-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-interlocked-l1-1-0.dll
new file mode 100644
index 0000000000..62d0d8ab33
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-interlocked-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-libraryloader-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-libraryloader-l1-1-0.dll
new file mode 100644
index 0000000000..054fca5544
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-libraryloader-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-localization-l1-2-0.dll b/lib/msvcp140/x64/api-ms-win-core-localization-l1-2-0.dll
new file mode 100644
index 0000000000..474775600f
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-localization-l1-2-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-memory-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-memory-l1-1-0.dll
new file mode 100644
index 0000000000..41b3a5bbc0
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-memory-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-namedpipe-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-namedpipe-l1-1-0.dll
new file mode 100644
index 0000000000..c89a74c7bb
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-namedpipe-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-processenvironment-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-processenvironment-l1-1-0.dll
new file mode 100644
index 0000000000..a820084ebe
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-processenvironment-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-processthreads-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-processthreads-l1-1-0.dll
new file mode 100644
index 0000000000..debbd17a3c
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-processthreads-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-processthreads-l1-1-1.dll b/lib/msvcp140/x64/api-ms-win-core-processthreads-l1-1-1.dll
new file mode 100644
index 0000000000..f7a2395b80
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-processthreads-l1-1-1.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-profile-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-profile-l1-1-0.dll
new file mode 100644
index 0000000000..99d2336dad
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-profile-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-rtlsupport-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-rtlsupport-l1-1-0.dll
new file mode 100644
index 0000000000..2352c908c4
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-rtlsupport-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-string-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-string-l1-1-0.dll
new file mode 100644
index 0000000000..2af9f458f9
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-string-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-synch-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-synch-l1-1-0.dll
new file mode 100644
index 0000000000..86a893a946
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-synch-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-synch-l1-2-0.dll b/lib/msvcp140/x64/api-ms-win-core-synch-l1-2-0.dll
new file mode 100644
index 0000000000..70d2f4b705
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-synch-l1-2-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-sysinfo-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-sysinfo-l1-1-0.dll
new file mode 100644
index 0000000000..400883dd7d
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-sysinfo-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-timezone-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-timezone-l1-1-0.dll
new file mode 100644
index 0000000000..3543bf6b86
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-timezone-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-core-util-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-core-util-l1-1-0.dll
new file mode 100644
index 0000000000..3932409597
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-core-util-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-conio-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-conio-l1-1-0.dll
new file mode 100644
index 0000000000..fee9507d8c
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-conio-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-convert-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-convert-l1-1-0.dll
new file mode 100644
index 0000000000..e398c3b6da
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-convert-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-environment-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-environment-l1-1-0.dll
new file mode 100644
index 0000000000..ee25a451bc
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-environment-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-filesystem-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-filesystem-l1-1-0.dll
new file mode 100644
index 0000000000..d8e225f362
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-filesystem-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-heap-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-heap-l1-1-0.dll
new file mode 100644
index 0000000000..f568d6d5b1
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-heap-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-locale-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-locale-l1-1-0.dll
new file mode 100644
index 0000000000..d11e4ba9ab
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-locale-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-math-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-math-l1-1-0.dll
new file mode 100644
index 0000000000..514e2d4530
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-math-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-multibyte-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-multibyte-l1-1-0.dll
new file mode 100644
index 0000000000..1234fb96b8
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-multibyte-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-private-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-private-l1-1-0.dll
new file mode 100644
index 0000000000..95c2067a08
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-private-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-process-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-process-l1-1-0.dll
new file mode 100644
index 0000000000..abbbf7cabf
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-process-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-runtime-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-runtime-l1-1-0.dll
new file mode 100644
index 0000000000..87f9144ab5
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-runtime-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-stdio-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-stdio-l1-1-0.dll
new file mode 100644
index 0000000000..a03d17c160
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-stdio-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-string-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-string-l1-1-0.dll
new file mode 100644
index 0000000000..19fff9dae1
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-string-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-time-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-time-l1-1-0.dll
new file mode 100644
index 0000000000..faf52046b6
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-time-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/api-ms-win-crt-utility-l1-1-0.dll b/lib/msvcp140/x64/api-ms-win-crt-utility-l1-1-0.dll
new file mode 100644
index 0000000000..772b23e15e
Binary files /dev/null and b/lib/msvcp140/x64/api-ms-win-crt-utility-l1-1-0.dll differ
diff --git a/lib/msvcp140/x64/concrt140.dll b/lib/msvcp140/x64/concrt140.dll
new file mode 100644
index 0000000000..915a13a3a1
Binary files /dev/null and b/lib/msvcp140/x64/concrt140.dll differ
diff --git a/lib/msvcp140/x64/msvcp140.dll b/lib/msvcp140/x64/msvcp140.dll
new file mode 100644
index 0000000000..4a5ca4f47a
Binary files /dev/null and b/lib/msvcp140/x64/msvcp140.dll differ
diff --git a/lib/msvcp140/x64/ucrtbase.dll b/lib/msvcp140/x64/ucrtbase.dll
new file mode 100644
index 0000000000..4c0f926221
Binary files /dev/null and b/lib/msvcp140/x64/ucrtbase.dll differ
diff --git a/lib/msvcp140/x64/vccorlib140.dll b/lib/msvcp140/x64/vccorlib140.dll
new file mode 100644
index 0000000000..d3cc8e00f1
Binary files /dev/null and b/lib/msvcp140/x64/vccorlib140.dll differ
diff --git a/lib/msvcp140/x64/vcruntime140.dll b/lib/msvcp140/x64/vcruntime140.dll
new file mode 100644
index 0000000000..52fe06bd61
Binary files /dev/null and b/lib/msvcp140/x64/vcruntime140.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-console-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-console-l1-1-0.dll
new file mode 100644
index 0000000000..eaedb39ccb
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-console-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-datetime-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-datetime-l1-1-0.dll
new file mode 100644
index 0000000000..211c00fabf
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-datetime-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-debug-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-debug-l1-1-0.dll
new file mode 100644
index 0000000000..204a23e2c9
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-debug-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-errorhandling-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-errorhandling-l1-1-0.dll
new file mode 100644
index 0000000000..7312ab3827
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-errorhandling-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-file-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-file-l1-1-0.dll
new file mode 100644
index 0000000000..a345dc6e71
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-file-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-file-l1-2-0.dll b/lib/msvcp140/x86/api-ms-win-core-file-l1-2-0.dll
new file mode 100644
index 0000000000..8dfc671c82
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-file-l1-2-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-file-l2-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-file-l2-1-0.dll
new file mode 100644
index 0000000000..5000625959
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-file-l2-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-handle-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-handle-l1-1-0.dll
new file mode 100644
index 0000000000..8e4d1ac4a7
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-handle-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-heap-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-heap-l1-1-0.dll
new file mode 100644
index 0000000000..2f8dba8767
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-heap-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-interlocked-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-interlocked-l1-1-0.dll
new file mode 100644
index 0000000000..abf946796e
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-interlocked-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-libraryloader-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-libraryloader-l1-1-0.dll
new file mode 100644
index 0000000000..ad39dbc73b
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-libraryloader-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-localization-l1-2-0.dll b/lib/msvcp140/x86/api-ms-win-core-localization-l1-2-0.dll
new file mode 100644
index 0000000000..df914950ca
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-localization-l1-2-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-memory-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-memory-l1-1-0.dll
new file mode 100644
index 0000000000..7f6cd01ce6
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-memory-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-namedpipe-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-namedpipe-l1-1-0.dll
new file mode 100644
index 0000000000..d1d0e7771d
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-namedpipe-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-processenvironment-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-processenvironment-l1-1-0.dll
new file mode 100644
index 0000000000..88a0809e05
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-processenvironment-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-processthreads-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-processthreads-l1-1-0.dll
new file mode 100644
index 0000000000..99dbfe6024
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-processthreads-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-processthreads-l1-1-1.dll b/lib/msvcp140/x86/api-ms-win-core-processthreads-l1-1-1.dll
new file mode 100644
index 0000000000..2c8315bdeb
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-processthreads-l1-1-1.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-profile-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-profile-l1-1-0.dll
new file mode 100644
index 0000000000..fbdc2fa98a
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-profile-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-rtlsupport-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-rtlsupport-l1-1-0.dll
new file mode 100644
index 0000000000..5538941b0c
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-rtlsupport-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-string-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-string-l1-1-0.dll
new file mode 100644
index 0000000000..ef94ad4d27
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-string-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-synch-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-synch-l1-1-0.dll
new file mode 100644
index 0000000000..f3abf477c5
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-synch-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-synch-l1-2-0.dll b/lib/msvcp140/x86/api-ms-win-core-synch-l1-2-0.dll
new file mode 100644
index 0000000000..239ee9f913
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-synch-l1-2-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-sysinfo-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-sysinfo-l1-1-0.dll
new file mode 100644
index 0000000000..7b4e1c9858
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-sysinfo-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-timezone-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-timezone-l1-1-0.dll
new file mode 100644
index 0000000000..feb24fc43f
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-timezone-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-core-util-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-core-util-l1-1-0.dll
new file mode 100644
index 0000000000..fedd4038bd
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-core-util-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-conio-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-conio-l1-1-0.dll
new file mode 100644
index 0000000000..08f68be2c2
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-conio-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-convert-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-convert-l1-1-0.dll
new file mode 100644
index 0000000000..5b4e62cc1d
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-convert-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-environment-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-environment-l1-1-0.dll
new file mode 100644
index 0000000000..8009912352
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-environment-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-filesystem-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-filesystem-l1-1-0.dll
new file mode 100644
index 0000000000..a4414560b5
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-filesystem-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-heap-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-heap-l1-1-0.dll
new file mode 100644
index 0000000000..2584aad585
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-heap-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-locale-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-locale-l1-1-0.dll
new file mode 100644
index 0000000000..9dcda52733
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-locale-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-math-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-math-l1-1-0.dll
new file mode 100644
index 0000000000..d916a4df87
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-math-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-multibyte-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-multibyte-l1-1-0.dll
new file mode 100644
index 0000000000..a6f53c2a62
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-multibyte-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-private-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-private-l1-1-0.dll
new file mode 100644
index 0000000000..7337a284a4
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-private-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-process-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-process-l1-1-0.dll
new file mode 100644
index 0000000000..e949c9aa8d
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-process-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-runtime-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-runtime-l1-1-0.dll
new file mode 100644
index 0000000000..a0930f4199
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-runtime-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-stdio-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-stdio-l1-1-0.dll
new file mode 100644
index 0000000000..7a47fa3aa1
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-stdio-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-string-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-string-l1-1-0.dll
new file mode 100644
index 0000000000..a00fd9eee2
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-string-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-time-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-time-l1-1-0.dll
new file mode 100644
index 0000000000..4b3c00b6d2
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-time-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/api-ms-win-crt-utility-l1-1-0.dll b/lib/msvcp140/x86/api-ms-win-crt-utility-l1-1-0.dll
new file mode 100644
index 0000000000..b285e9cafc
Binary files /dev/null and b/lib/msvcp140/x86/api-ms-win-crt-utility-l1-1-0.dll differ
diff --git a/lib/msvcp140/x86/concrt140.dll b/lib/msvcp140/x86/concrt140.dll
new file mode 100644
index 0000000000..4f9ed11ade
Binary files /dev/null and b/lib/msvcp140/x86/concrt140.dll differ
diff --git a/lib/msvcp140/x86/msvcp140.dll b/lib/msvcp140/x86/msvcp140.dll
index d2cd9cdcdb..8501b34b3c 100644
Binary files a/lib/msvcp140/x86/msvcp140.dll and b/lib/msvcp140/x86/msvcp140.dll differ
diff --git a/lib/msvcp140/x86/ucrtbase.dll b/lib/msvcp140/x86/ucrtbase.dll
new file mode 100644
index 0000000000..92c99d0cab
Binary files /dev/null and b/lib/msvcp140/x86/ucrtbase.dll differ
diff --git a/lib/msvcp140/x86/vccorlib140.dll b/lib/msvcp140/x86/vccorlib140.dll
new file mode 100644
index 0000000000..48f5789c67
Binary files /dev/null and b/lib/msvcp140/x86/vccorlib140.dll differ
diff --git a/lib/msvcp140/x86/vcruntime140.dll b/lib/msvcp140/x86/vcruntime140.dll
new file mode 100644
index 0000000000..21c18f7e82
Binary files /dev/null and b/lib/msvcp140/x86/vcruntime140.dll differ
diff --git a/lib/nuget/nuget.exe b/lib/nuget/nuget.exe
new file mode 100644
index 0000000000..9f8781de0d
Binary files /dev/null and b/lib/nuget/nuget.exe differ
diff --git a/lib/sqlce/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest b/lib/sqlce/x64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest
similarity index 100%
rename from lib/sqlce/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest
rename to lib/sqlce/x64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest
diff --git a/lib/sqlce/amd64/Microsoft.VC90.CRT/README_ENU.txt b/lib/sqlce/x64/Microsoft.VC90.CRT/README_ENU.txt
similarity index 100%
rename from lib/sqlce/amd64/Microsoft.VC90.CRT/README_ENU.txt
rename to lib/sqlce/x64/Microsoft.VC90.CRT/README_ENU.txt
diff --git a/lib/sqlce/amd64/Microsoft.VC90.CRT/msvcr90.dll b/lib/sqlce/x64/Microsoft.VC90.CRT/msvcr90.dll
similarity index 100%
rename from lib/sqlce/amd64/Microsoft.VC90.CRT/msvcr90.dll
rename to lib/sqlce/x64/Microsoft.VC90.CRT/msvcr90.dll
diff --git a/lib/sqlce/amd64/sqlceca40.dll b/lib/sqlce/x64/sqlceca40.dll
similarity index 100%
rename from lib/sqlce/amd64/sqlceca40.dll
rename to lib/sqlce/x64/sqlceca40.dll
diff --git a/lib/sqlce/amd64/sqlcecompact40.dll b/lib/sqlce/x64/sqlcecompact40.dll
similarity index 100%
rename from lib/sqlce/amd64/sqlcecompact40.dll
rename to lib/sqlce/x64/sqlcecompact40.dll
diff --git a/lib/sqlce/amd64/sqlceer40EN.dll b/lib/sqlce/x64/sqlceer40EN.dll
similarity index 100%
rename from lib/sqlce/amd64/sqlceer40EN.dll
rename to lib/sqlce/x64/sqlceer40EN.dll
diff --git a/lib/sqlce/amd64/sqlceme40.dll b/lib/sqlce/x64/sqlceme40.dll
similarity index 100%
rename from lib/sqlce/amd64/sqlceme40.dll
rename to lib/sqlce/x64/sqlceme40.dll
diff --git a/lib/sqlce/amd64/sqlceqp40.dll b/lib/sqlce/x64/sqlceqp40.dll
similarity index 100%
rename from lib/sqlce/amd64/sqlceqp40.dll
rename to lib/sqlce/x64/sqlceqp40.dll
diff --git a/lib/sqlce/amd64/sqlcese40.dll b/lib/sqlce/x64/sqlcese40.dll
similarity index 100%
rename from lib/sqlce/amd64/sqlcese40.dll
rename to lib/sqlce/x64/sqlcese40.dll
diff --git a/lib/vswhere/vswhere.exe b/lib/vswhere/vswhere.exe
new file mode 100644
index 0000000000..3d91a17dc4
Binary files /dev/null and b/lib/vswhere/vswhere.exe 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 f74f0edf78..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("2.6.0.0")]
+[assembly: AssemblyVersion("3.2.2.0")]
-[assembly: AssemblyFileVersion("2.6.0.0")]
-[assembly: AssemblyInformationalVersion("2.6.0.0")]
+[assembly: AssemblyFileVersion("3.2.2.0")]
+[assembly: AssemblyInformationalVersion("3.2.2.0")]
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs b/src/Libraries/SmartStore.Core/Async/AsyncLock.cs
deleted file mode 100644
index b93da92afc..0000000000
--- a/src/Libraries/SmartStore.Core/Async/AsyncLock.cs
+++ /dev/null
@@ -1,110 +0,0 @@
-using System;
-using System.Collections.Concurrent;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-using SmartStore.Utilities;
-
-namespace SmartStore.Core.Async
-{
- public sealed class AsyncLock : DisposableObject
- {
- private static readonly TimeSpan _noTimeout = TimeSpan.FromMilliseconds(-1);
- private static readonly ConcurrentDictionary _keyLocks = new ConcurrentDictionary();
-
- private readonly SemaphoreSlim _semaphore;
- private readonly int _initialCount;
-
- public AsyncLock()
- : this(1, int.MaxValue)
- {
- }
-
- public AsyncLock(int initialCount)
- : this(initialCount, int.MaxValue)
- {
- }
-
- public AsyncLock(int initialCount, int maxCount)
- {
- _semaphore = new SemaphoreSlim(initialCount, maxCount);
- _initialCount = initialCount;
- }
-
- public static AsyncLock Acquire(object key)
- {
- Guard.NotNull(key, nameof(key));
-
- var asyncLock = _keyLocks.GetOrAdd(key, x => new AsyncLock());
-
- // Perf: once we have more than 100 lock objects in the storage, remove the idle ones.
- if (_keyLocks.Count > 100)
- {
- // never remove the current one and the locked ones
- var idles = _keyLocks.Where(kvp => kvp.Key != key && !kvp.Value.IsLockHeld).ToArray();
- foreach (var kvp in idles)
- {
- var value = kvp.Value;
- // double check for held lock
- if (!value.IsLockHeld)
- {
- _keyLocks.TryRemove(kvp.Key, out value);
- }
- }
- }
-
- return asyncLock;
- }
-
- public bool IsLockHeld
- {
- get { return _semaphore.CurrentCount < _initialCount; }
- }
-
- public int Release()
- {
- return _semaphore.Release();
- }
-
- public int Release(int releaseCount)
- {
- return _semaphore.Release(releaseCount);
- }
-
- public Task LockAsync()
- {
- return LockAsync(_noTimeout, CancellationToken.None);
- }
-
- public Task LockAsync(TimeSpan timeout)
- {
- return LockAsync(timeout, CancellationToken.None);
- }
-
- public Task LockAsync(CancellationToken cancellationToken)
- {
- return LockAsync(_noTimeout, cancellationToken);
- }
-
- public Task LockAsync(TimeSpan timeout, CancellationToken cancellationToken)
- {
- var wait = _semaphore.WaitAsync(timeout, cancellationToken);
-
- return wait.IsCompleted
- ? Task.FromResult(new ActionDisposable(() => _semaphore.Release()) as IDisposable)
- : wait.ContinueWith((t, s) => new ActionDisposable(() => ((SemaphoreSlim)s).Release()) as IDisposable,
- _semaphore,
- CancellationToken.None,
- TaskContinuationOptions.ExecuteSynchronously,
- TaskScheduler.Default);
- }
-
- protected override void OnDispose(bool disposing)
- {
- if (disposing)
- _semaphore.Dispose();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
index 1fce2cc436..2751259eef 100644
--- a/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
+++ b/src/Libraries/SmartStore.Core/Async/AsyncRunner.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,113 +9,166 @@
namespace SmartStore.Core.Async
{
- public static class AsyncRunner
+ using EventQueue = ConcurrentQueue>;
+
+ public class AsyncRunner : IDisposable
{
- private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+ #region Instance members
- ///
- /// Gets the global cancellation token which signals the application shutdown
- ///
- public static CancellationToken AppShutdownCancellationToken
+ private ExclusiveSynchronizationContext CurrentContext;
+ private SynchronizationContext OldContext;
+ private int TaskCount;
+
+ private AsyncRunner()
{
- get { return _host.ShutdownCancellationTokenSource.Token; }
+ OldContext = SynchronizationContext.Current;
+ CurrentContext = new ExclusiveSynchronizationContext(OldContext);
+ SynchronizationContext.SetSynchronizationContext(CurrentContext);
+ }
+
+ public void Dispose()
+ {
+ try
+ {
+ CurrentContext.BeginMessageLoop();
+ }
+ catch (Exception e)
+ {
+ throw e;
+ }
+ finally
+ {
+ SynchronizationContext.SetSynchronizationContext(OldContext);
+ }
+ }
+
+ private void Increment()
+ {
+ Interlocked.Increment(ref TaskCount);
+ }
+
+ private void Decrement()
+ {
+ Interlocked.Decrement(ref TaskCount);
+ if (TaskCount == 0)
+ {
+ CurrentContext.EndMessageLoop();
+ }
}
///
/// Executes an async Task method which has a void return value synchronously
///
- /// Task method to execute
- public static void RunSync(Func func)
+ /// Task execute
+ public void Run(Task task, Action continuation = null)
{
- var oldContext = SynchronizationContext.Current;
- var synch = new ExclusiveSynchronizationContext();
- SynchronizationContext.SetSynchronizationContext(synch);
- synch.Post(async _ =>
+ CurrentContext.Post(async _ =>
{
try
{
- await func();
+ Increment();
+ await task;
+
+ continuation?.Invoke(task);
}
catch (Exception e)
{
- synch.InnerException = e;
- throw;
+ CurrentContext.InnerException = e;
}
finally
{
- synch.EndMessageLoop();
+ Decrement();
}
}, null);
- synch.BeginMessageLoop();
-
- SynchronizationContext.SetSynchronizationContext(oldContext);
}
///
/// Executes an async Task method which has a TResult return type synchronously
///
/// Return Type
- /// Task method to execute
- ///
- public static TResult RunSync(Func> func)
+ /// Task to execute
+ public TResult Run(Task task, Action> continuation = null)
{
- var oldContext = SynchronizationContext.Current;
- var synch = new ExclusiveSynchronizationContext();
- SynchronizationContext.SetSynchronizationContext(synch);
- TResult ret = default(TResult);
- synch.Post(async _ =>
+ var result = default(TResult);
+
+ CurrentContext.Post(async _ =>
{
try
{
- ret = await func();
+ Increment();
+ result = await task;
+
+ continuation?.Invoke(task);
}
catch (Exception e)
{
- synch.InnerException = e;
- throw;
+ CurrentContext.InnerException = e;
}
finally
{
- synch.EndMessageLoop();
+ Decrement();
}
}, null);
- synch.BeginMessageLoop();
- SynchronizationContext.SetSynchronizationContext(oldContext);
- return ret;
- }
- public static Task Run(Action action)
- {
- return Run(action, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
+ return result;
}
- public static Task Run(Action action, CancellationToken cancellationToken)
+ #endregion
+
+ private static readonly BackgroundWorkHost _host = new BackgroundWorkHost();
+
+ ///
+ /// Gets the global cancellation token which signals the application shutdown
+ ///
+ public static CancellationToken AppShutdownCancellationToken
{
- return Run(action, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
+ get { return _host.ShutdownCancellationTokenSource.Token; }
}
- public static Task Run(Action action, TaskCreationOptions options)
+ public static AsyncRunner Create()
{
- return Run(action, CancellationToken.None, options, TaskScheduler.Default);
+ return new AsyncRunner();
}
- public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options)
+ ///
+ /// Executes an async Task method which has a void return value synchronously
+ ///
+ /// Task method to execute
+ public static void RunSync(Func func, Action continuation = null)
{
- return Run(action, cancellationToken, options, TaskScheduler.Default);
+ using (var runner = new AsyncRunner())
+ {
+ runner.Run(func(), continuation);
+ }
}
- public static Task Run(Action action, TaskScheduler scheduler)
+ ///
+ /// Executes an async Task method which has a TResult return type synchronously
+ ///
+ /// Return Type
+ /// Task method to execute
+ ///
+ public static TResult RunSync(Func> func, Action> continuation = null)
{
- return Run(action, CancellationToken.None, TaskCreationOptions.None, scheduler);
+ var result = default(TResult);
+
+ using (var runner = new AsyncRunner())
+ {
+ result = runner.Run(func(), continuation);
+ }
+
+ return result;
}
- public static Task Run(Action action, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Action action,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(action, nameof(action));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew(() => {
var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
@@ -122,21 +176,24 @@ public static Task Run(Action action, Cancell
{
action(accessor.GetLifetimeScope(null), ct);
}
- }, ct, options, scheduler);
+ }, ct, options, scheduler ?? TaskScheduler.Default);
_host.Register(t, ct);
return t;
}
- public static Task Run(Action action, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Action action,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(state, nameof(state));
Guard.NotNull(action, nameof(action));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew((o) =>
{
@@ -145,47 +202,22 @@ public static Task Run(Action action,
{
action(accessor.GetLifetimeScope(null), ct, o);
}
- }, state, ct, options, scheduler);
+ }, state, ct, options, scheduler ?? TaskScheduler.Default);
_host.Register(t, ct);
return t;
}
-
-
- public static Task Run(Func function)
- {
- return Run(function, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken)
- {
- return Run(function, cancellationToken, TaskCreationOptions.None, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, TaskCreationOptions options)
- {
- return Run(function, CancellationToken.None, options, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options)
- {
- return Run(function, cancellationToken, options, TaskScheduler.Default);
- }
-
- public static Task Run(Func function, TaskScheduler scheduler)
- {
- return Run(function, CancellationToken.None, TaskCreationOptions.None, scheduler);
- }
-
- public static Task Run(Func function, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Func function,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(function, nameof(function));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew(() =>
{
@@ -194,21 +226,24 @@ public static Task Run(Func Run(Func function, object state, CancellationToken cancellationToken, TaskCreationOptions options, TaskScheduler scheduler)
+ public static Task Run(
+ Func function,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken),
+ TaskCreationOptions options = TaskCreationOptions.LongRunning,
+ TaskScheduler scheduler = null)
{
Guard.NotNull(state, nameof(state));
Guard.NotNull(function, nameof(function));
- Guard.NotNull(scheduler, nameof(scheduler));
var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
- options |= TaskCreationOptions.LongRunning; // enforce an exclusive thread (not from pool)
var t = Task.Factory.StartNew((o) =>
{
@@ -217,19 +252,61 @@ public static Task Run(Func function,
+ object state,
+ CancellationToken cancellationToken = default(CancellationToken))
+ {
+ Guard.NotNull(state, nameof(state));
+ Guard.NotNull(function, nameof(function));
+
+ var ct = _host.CreateCompositeCancellationTokenSource(cancellationToken).Token;
+
+ var accessor = EngineContext.Current.ContainerManager.ScopeAccessor;
+ var scope = accessor.BeginLifetimeScope(null);
+
+ Task task = null;
+
+ try
+ {
+ task = function(scope, ct).ContinueWith(x =>
+ {
+ scope.Dispose();
+ });
+ _host.Register(task, ct);
+ }
+ catch
+ {
+ scope.Dispose();
+ }
+
+ return task;
+ }
private class ExclusiveSynchronizationContext : SynchronizationContext
{
private bool _done;
- readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
- readonly Queue> _items = new Queue>();
+ private readonly AutoResetEvent _workItemsWaiting = new AutoResetEvent(false);
+ private readonly EventQueue _items;
+
+ public ExclusiveSynchronizationContext(SynchronizationContext old)
+ {
+ if (old is ExclusiveSynchronizationContext oldEx)
+ {
+ this._items = oldEx._items;
+ }
+ else
+ {
+ this._items = new EventQueue();
+ }
+ }
public Exception InnerException { get; set; }
@@ -240,10 +317,7 @@ public override void Send(SendOrPostCallback d, object state)
public override void Post(SendOrPostCallback d, object state)
{
- lock (_items)
- {
- _items.Enqueue(Tuple.Create(d, state));
- }
+ _items.Enqueue(Tuple.Create(d, state));
_workItemsWaiting.Set();
}
@@ -256,26 +330,19 @@ public void BeginMessageLoop()
{
while (!_done)
{
- Tuple task = null;
- lock (_items)
+ if (!_items.TryDequeue(out var task))
{
- if (_items.Count > 0)
- {
- task = _items.Dequeue();
- }
+ _workItemsWaiting.WaitOne();
}
- if (task != null)
+ else
{
task.Item1(task.Item2);
- if (InnerException != null) // the method threw an exeption
+
+ if (InnerException != null) // method threw an exeption
{
throw new AggregateException("AsyncHelpers.Run method threw an exception.", InnerException);
}
}
- else
- {
- _workItemsWaiting.WaitOne();
- }
}
}
@@ -291,7 +358,7 @@ internal class BackgroundWorkHost : IRegisteredObject
{
private readonly CancellationTokenSource _shutdownCancellationTokenSource = new CancellationTokenSource();
private int _numRunningWorkItems;
-
+
public BackgroundWorkHost()
{
HostingEnvironment.RegisterObject(this);
@@ -337,7 +404,7 @@ public void Register(Task work, CancellationToken cancellationToken)
}
_numRunningWorkItems++;
}
-
+
work.ContinueWith(
WorkItemComplete,
CancellationToken.None,
@@ -353,7 +420,7 @@ private void WorkItemComplete(Task work)
lock (this)
{
num = --_numRunningWorkItems;
- isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
+ isCancellationRequested = _shutdownCancellationTokenSource.IsCancellationRequested;
}
if (num == 0 && isCancellationRequested)
{
diff --git a/src/Libraries/SmartStore.Core/BaseEntity.cs b/src/Libraries/SmartStore.Core/BaseEntity.cs
index 0c90dc9a9a..3fd0016909 100644
--- a/src/Libraries/SmartStore.Core/BaseEntity.cs
+++ b/src/Libraries/SmartStore.Core/BaseEntity.cs
@@ -1,18 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
+using System.Data.Entity.Core.Objects;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.Serialization;
namespace SmartStore.Core
-{
-
+{
///
/// Base class for entities
///
[DataContract]
- public abstract partial class BaseEntity
+ public abstract partial class BaseEntity : IEquatable
{
///
/// Gets or sets the entity identifier
@@ -21,20 +21,30 @@ public abstract partial class BaseEntity
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
- [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
+ [SuppressMessage("ReSharper", "PossibleNullReferenceException")]
+ public virtual string GetEntityName()
+ {
+ return GetUnproxiedType().Name;
+ }
+
public Type GetUnproxiedType()
{
- var t = GetType();
- if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity."))
- {
- // it's a proxied type
- t = t.BaseType;
- }
- return t;
- }
+ #region Old
+ //var t = GetType();
+ //if (t.AssemblyQualifiedName.StartsWith("System.Data.Entity."))
+ //{
+ // // it's a proxied type
+ // t = t.BaseType;
+ //}
+
+ //return t;
+ #endregion
+
+ return ObjectContext.GetObjectType(GetType());
+ }
///
- /// Transient objects are not associated with an item already in storage. For instance,
+ /// Transient objects are not associated with an item already in storage. For instance,
/// a Product entity is transient if its Id is 0.
///
public virtual bool IsTransientRecord()
@@ -44,28 +54,33 @@ public virtual bool IsTransientRecord()
public override bool Equals(object obj)
{
- return Equals(obj as BaseEntity);
+ return this.Equals(obj as BaseEntity);
}
- protected virtual bool Equals(BaseEntity other)
- {
- if (other == null)
- return false;
+ bool IEquatable.Equals(BaseEntity other)
+ {
+ return this.Equals(other);
+ }
- if (ReferenceEquals(this, other))
- return true;
+ protected virtual bool Equals(BaseEntity other)
+ {
+ if (other == null)
+ return false;
- if (HasSameNonDefaultIds(other))
- {
- var otherType = other.GetUnproxiedType();
- var thisType = GetUnproxiedType();
- return thisType.Equals(otherType);
- }
+ if (ReferenceEquals(this, other))
+ return true;
- return false;
- }
+ if (HasSameNonDefaultIds(other))
+ {
+ var otherType = other.GetUnproxiedType();
+ var thisType = GetUnproxiedType();
+ return thisType.Equals(otherType);
+ }
+
+ return false;
+ }
- [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
+ [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override int GetHashCode()
{
if (IsTransientRecord())
@@ -99,5 +114,5 @@ private bool HasSameNonDefaultIds(BaseEntity other)
{
return !this.IsTransientRecord() && !other.IsTransientRecord() && this.Id == other.Id;
}
- }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Caching/CacheScope.cs b/src/Libraries/SmartStore.Core/Caching/CacheScope.cs
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 a2e151ca65..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
@@ -69,17 +76,34 @@ public interface ICacheManager
void Remove(string key);
///
- /// Scans for all all keys in the underlying cache
+ /// Scans for all keys matching the input pattern
///
/// A key pattern. Can be null .
- /// The sequence of matching keys
- string[] Keys(string pattern);
+ /// An array of matching key names
+ ///
+ /// Supported glob-style patterns:
+ /// - h?llo matches hello, hallo and hxllo
+ /// - h*llo matches hllo and heeeello
+ /// - h[ae]llo matches hello and hallo, but not hillo
+ /// - h[^e]llo matches hallo, hbllo, ... but not hello
+ /// - h[a-b]llo matches hallo and hbllo
+ ///
+ IEnumerable Keys(string pattern);
///
- /// Removes items by pattern
+ /// Removes all entries with keys matching the input pattern
///
- /// pattern
- void RemoveByPattern(string pattern);
+ /// Glob pattern
+ /// Count of removed cache items
+ ///
+ /// Supported glob-style patterns:
+ /// - h?llo matches hello, hallo and hxllo
+ /// - h*llo matches hllo and heeeello
+ /// - h[ae]llo matches hello and hallo, but not hillo
+ /// - h[^e]llo matches hallo, hbllo, ... but not hello
+ /// - h[a-b]llo matches hallo and hbllo
+ ///
+ int RemoveByPattern(string pattern);
///
/// Clear all cache data
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 63e6949a95..a00224784d 100644
--- a/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
+++ b/src/Libraries/SmartStore.Core/Caching/MemoryCacheManager.cs
@@ -4,25 +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
@@ -30,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);
@@ -38,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;
@@ -50,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)
@@ -121,7 +148,7 @@ public void Remove(string key)
_cache.Remove(key);
}
- public string[] Keys(string pattern)
+ public IEnumerable Keys(string pattern)
{
Guard.NotEmpty(pattern, nameof(pattern));
@@ -132,35 +159,54 @@ public string[] Keys(string pattern)
return keys.ToArray();
}
- return keys.Where(x => x.StartsWith(pattern, StringComparison.OrdinalIgnoreCase)).ToArray();
+ var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
+ return keys.Where(x => wildcard.IsMatch(x)).ToArray();
}
- public void RemoveByPattern(string pattern)
+ public int RemoveByPattern(string pattern)
{
- var keysToRemove = Keys(pattern);
-
lock (_cache)
{
+ var keysToRemove = Keys(pattern);
+ int count = 0;
+
// lock atomic operation
foreach (string key in keysToRemove)
{
_cache.Remove(key);
+ count++;
}
- }
- }
+
+ return count;
+ }
+ }
public void Clear()
{
- 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;
@@ -168,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 d04b698ca7..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)
{
}
@@ -65,13 +65,14 @@ public void Remove(string key)
{
}
- public string[] Keys(string pattern)
+ public IEnumerable Keys(string pattern)
{
return new string[0];
}
- public void RemoveByPattern(string pattern)
+ public int RemoveByPattern(string pattern)
{
+ return 0;
}
public void Clear()
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
index 67e7f5a806..9629fee3c5 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/DisplayControl.cs
@@ -1,57 +1,185 @@
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
+using Autofac;
using SmartStore.Core.Data;
using SmartStore.Core.Domain.Blogs;
using SmartStore.Core.Domain.Catalog;
using SmartStore.Core.Domain.Discounts;
+using SmartStore.Core.Domain.Localization;
using SmartStore.Core.Domain.News;
using SmartStore.Core.Domain.Topics;
using SmartStore.Utilities;
namespace SmartStore.Core.Caching
{
+ public delegate IEnumerable DisplayControlHandler(BaseEntity entity, IComponentContext ctx);
+
public partial class DisplayControl : IDisplayControl
{
- public static readonly HashSet CandidateTypes = new HashSet(new Type[]
- {
- typeof(BlogComment),
- typeof(BlogPost),
- typeof(Category),
- typeof(Manufacturer),
- typeof(Product),
- typeof(ProductBundleItem),
- typeof(ProductPicture),
- typeof(SpecificationAttribute),
- typeof(ProductSpecificationAttribute),
- typeof(SpecificationAttributeOption),
- typeof(ProductVariantAttribute),
- typeof(ProductVariantAttributeValue),
- typeof(ProductVariantAttributeCombination),
- typeof(TierPrice),
- typeof(Discount),
- typeof(CrossSellProduct),
- typeof(RelatedProduct),
- typeof(ProductCategory),
- typeof(ProductManufacturer),
- typeof(NewsItem),
- typeof(NewsComment),
- typeof(Topic)
- });
+ private static readonly ConcurrentDictionary _handlers = new ConcurrentDictionary
+ {
+ [typeof(BlogComment)] = (x, c) => new[] { "b" + ((BlogComment)x).BlogPostId },
+ [typeof(BlogPost)] = (x, c) => new[] { "b" + x.Id },
+ [typeof(Category)] = (x, c) => new[] { "c" + x.Id },
+ [typeof(Manufacturer)] = (x, c) => new[] { "m" + x.Id },
+ [typeof(ProductBundleItem)] = (x, c) => new[] { "p" + ((ProductBundleItem)x).ProductId },
+ [typeof(ProductPicture)] = (x, c) => new[] { "p" + ((ProductPicture)x).ProductId },
+ [typeof(ProductSpecificationAttribute)] = (x, c) => new[] { "p" + ((ProductSpecificationAttribute)x).ProductId },
+ [typeof(ProductVariantAttributeCombination)]= (x, c) => new[] { "p" + ((ProductVariantAttributeCombination)x).ProductId },
+ [typeof(TierPrice)] = (x, c) => new[] { "p" + ((TierPrice)x).ProductId },
+ [typeof(CrossSellProduct)] = (x, c) => new[] { "p" + ((CrossSellProduct)x).ProductId1, "p" + ((CrossSellProduct)x).ProductId2 },
+ [typeof(RelatedProduct)] = (x, c) => new[] { "p" + ((RelatedProduct)x).ProductId1, "p" + ((RelatedProduct)x).ProductId2 },
+ [typeof(ProductCategory)] = (x, c) => new[] { "p" + ((ProductCategory)x).CategoryId, "p" + ((ProductCategory)x).ProductId },
+ [typeof(ProductManufacturer)] = (x, c) => new[] { "p" + ((ProductManufacturer)x).ManufacturerId, "p" + ((ProductManufacturer)x).ProductId },
+ [typeof(NewsItem)] = (x, c) => new[] { "n" + x.Id },
+ [typeof(NewsComment)] = (x, c) => new[] { "n" + ((NewsComment)x).NewsItemId },
+ [typeof(Topic)] = (x, c) => new[] { "t" + x.Id },
+ [typeof(SpecificationAttributeOption)] = (x, c) => ((SpecificationAttributeOption)x).ProductSpecificationAttributes.Select(y => "p" + y.ProductId),
+ [typeof(ProductTag)] = (x, c) => ((ProductTag)x).Products.Select(y => "p" + y.Id),
+ [typeof(Product)] = HandleProduct,
+ [typeof(SpecificationAttribute)] = HandleSpecificationAttribute,
+ [typeof(ProductVariantAttributeValue)] = HandleProductVariantAttributeValue,
+ [typeof(Discount)] = HandleDiscount,
+ [typeof(LocalizedProperty)] = HandleLocalizedProperty
+ };
private readonly HashSet _entities = new HashSet();
- private readonly Lazy> _rsProductSpecAttr;
+ private readonly IComponentContext _componentContext;
private bool _isIdle;
private bool? _isUncacheableRequest;
- public DisplayControl(Lazy> rsProductSpecAttr)
+ public DisplayControl(IComponentContext componentContext)
+ {
+ _componentContext = componentContext;
+ }
+
+ #region Static
+
+ public static bool ContainsHandlerFor(Type type)
+ {
+ Guard.NotNull(type, nameof(type));
+
+ return _handlers.ContainsKey(type);
+ }
+
+ public static void RegisterHandlerFor(Type type, DisplayControlHandler handler)
+ {
+ Guard.NotNull(type, nameof(type));
+ Guard.NotNull(handler, nameof(handler));
+
+ _handlers.TryAdd(type, handler);
+ }
+
+ #endregion
+
+ #region Handlers
+
+ private static IEnumerable HandleProduct(BaseEntity entity, IComponentContext ctx)
+ {
+ var product = ((Product)entity);
+ yield return "p" + entity.Id;
+ if (product.ProductType == ProductType.GroupedProduct && product.ParentGroupedProductId > 0)
+ {
+ yield return "p" + product.ParentGroupedProductId;
+ }
+ }
+
+ private static IEnumerable HandleSpecificationAttribute(BaseEntity entity, IComponentContext ctx)
+ {
+ // Determine all affected products (which are assigned to this attribute).
+ var specAttrId = ((SpecificationAttribute)entity).Id;
+ var affectedProductIds = ctx.Resolve().Set().AsNoTracking()
+ .Where(x => x.SpecificationAttributeOption.SpecificationAttribute.Id == specAttrId)
+ .Select(x => x.ProductId)
+ .Distinct()
+ .ToList();
+
+ foreach (var id in affectedProductIds)
+ {
+ yield return "p" + id;
+ }
+ }
+
+ private static IEnumerable HandleProductVariantAttributeValue(BaseEntity entity, IComponentContext ctx)
+ {
+ var pva = ((ProductVariantAttributeValue)entity).ProductVariantAttribute;
+ if (pva != null)
+ {
+ yield return "p" + pva.ProductId;
+ }
+ }
+
+ private static IEnumerable HandleDiscount(BaseEntity entity, IComponentContext ctx)
+ {
+ var discount = (Discount)entity;
+ if (discount.DiscountType == DiscountType.AssignedToCategories)
+ {
+ foreach (var category in discount.AppliedToCategories)
+ {
+ yield return "c" + category.Id;
+ }
+ }
+ else if (discount.DiscountType == DiscountType.AssignedToSkus)
+ {
+ foreach (var product in discount.AppliedToProducts)
+ {
+ yield return "p" + product.Id;
+ }
+ }
+ }
+
+ private static IEnumerable HandleLocalizedProperty(BaseEntity entity, IComponentContext ctx)
{
- _rsProductSpecAttr = rsProductSpecAttr;
+ var lp = (LocalizedProperty)entity;
+ string prefix = null;
+ BaseEntity targetEntity = null;
+
+ var dbContext = ctx.Resolve();
+
+ switch (lp.LocaleKeyGroup)
+ {
+ case nameof(Product):
+ prefix = "p";
+ break;
+ case nameof(Category):
+ prefix = "c";
+ break;
+ case nameof(Manufacturer):
+ prefix = "m";
+ break;
+ case nameof(Topic):
+ prefix = "t";
+ break;
+ case nameof(SpecificationAttribute):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ case nameof(SpecificationAttributeOption):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ case nameof(ProductVariantAttributeValue):
+ targetEntity = dbContext.Set().Find(lp.EntityId);
+ break;
+ }
+
+ if (prefix.HasValue())
+ {
+ yield return prefix + lp.EntityId;
+ }
+ else if (targetEntity != null)
+ {
+ var tags = ctx.Resolve().GetCacheControlTagsFor(targetEntity);
+ foreach (var tag in tags)
+ {
+ yield return tag;
+ }
+ }
}
+ #endregion
+
public IDisposable BeginIdleScope()
{
_isIdle = true;
@@ -91,149 +219,21 @@ public bool IsUncacheableRequest
public virtual IEnumerable GetCacheControlTagsFor(BaseEntity entity)
{
- Guard.NotNull(entity, nameof(entity));
+ var empty = Enumerable.Empty();
- if (entity.IsTransientRecord())
+ if (entity == null || entity.IsTransientRecord())
{
- yield break;
+ return empty;
}
var type = entity.GetUnproxiedType();
- if (!CandidateTypes.Contains(type))
+ if (!_handlers.TryGetValue(type, out var handler))
{
- yield break;
+ return empty;
}
- if (type == typeof(BlogComment))
- {
- yield return "b" + ((BlogComment)entity).BlogPostId;
- }
- else if (type == typeof(BlogPost))
- {
- yield return "b" + entity.Id;
- }
- else if (type == typeof(Category))
- {
- yield return "c" + entity.Id;
- }
- else if (type == typeof(Manufacturer))
- {
- yield return "m" + entity.Id;
- }
- else if (type == typeof(Product))
- {
- var product = ((Product)entity);
- yield return "p" + entity.Id;
- if (product.ProductType == ProductType.GroupedProduct && product.ParentGroupedProductId > 0)
- {
- yield return "p" + product.ParentGroupedProductId;
- }
- }
- else if (type == typeof(ProductBundleItem))
- {
- yield return "p" + ((ProductBundleItem)entity).ProductId;
- }
- else if (type == typeof(ProductPicture))
- {
- yield return "p" + ((ProductPicture)entity).ProductId;
- }
- else if (type == typeof(SpecificationAttribute))
- {
- // Determine all affected products (which are assigned to this attribute).
- var specAttrId = ((SpecificationAttribute)entity).Id;
- var affectedProductIds = _rsProductSpecAttr.Value.TableUntracked
- .Where(x => x.SpecificationAttributeOption.SpecificationAttribute.Id == specAttrId)
- .Select(x => x.ProductId)
- .Distinct()
- .ToList();
-
- foreach (var id in affectedProductIds)
- {
- yield return "p" + id;
- }
- }
- else if (type == typeof(ProductSpecificationAttribute))
- {
- yield return "p" + ((ProductSpecificationAttribute)entity).ProductId;
- }
- else if (type == typeof(SpecificationAttributeOption))
- {
- foreach (var attr in ((SpecificationAttributeOption)entity).ProductSpecificationAttributes)
- {
- yield return "p" + attr.ProductId;
- }
- }
- else if (type == typeof(ProductVariantAttribute))
- {
- yield return "p" + ((ProductVariantAttribute)entity).ProductId;
- }
- else if (type == typeof(ProductVariantAttributeValue))
- {
- yield return "p" + ((ProductVariantAttributeValue)entity).ProductVariantAttribute.ProductId;
- }
- else if (type == typeof(ProductVariantAttributeCombination))
- {
- yield return "p" + ((ProductVariantAttributeCombination)entity).ProductId;
- }
- else if (type == typeof(TierPrice))
- {
- yield return "p" + ((TierPrice)entity).ProductId;
- }
- else if (type == typeof(Discount))
- {
- var discount = (Discount)entity;
- if (discount.DiscountType == DiscountType.AssignedToCategories)
- {
- foreach (var category in discount.AppliedToCategories)
- {
- yield return "c" + category.Id;
- }
- }
- else if (discount.DiscountType == DiscountType.AssignedToSkus)
- {
- foreach (var product in discount.AppliedToProducts)
- {
- yield return "p" + product.Id;
- }
- }
- }
- else if (type == typeof(CrossSellProduct))
- {
- yield return "p" + ((CrossSellProduct)entity).ProductId1;
- yield return "p" + ((CrossSellProduct)entity).ProductId2;
- }
- else if (type == typeof(RelatedProduct))
- {
- yield return "p" + ((RelatedProduct)entity).ProductId1;
- yield return "p" + ((RelatedProduct)entity).ProductId2;
- }
- else if (type == typeof(ProductCategory))
- {
- yield return "c" + ((ProductCategory)entity).CategoryId;
- yield return "p" + ((ProductCategory)entity).ProductId;
- }
- else if (type == typeof(ProductManufacturer))
- {
- yield return "m" + ((ProductManufacturer)entity).ManufacturerId;
- yield return "p" + ((ProductManufacturer)entity).ProductId;
- }
- else if (type == typeof(NewsItem))
- {
- yield return "n" + entity.Id;
- }
- else if (type == typeof(NewsComment))
- {
- yield return "n" + ((NewsComment)entity).NewsItemId;
- }
- else if (type == typeof(Topic))
- {
- var topic = ((Topic)entity);
- if (!topic.RenderAsWidget)
- {
- yield return "t" + topic.Id;
- }
- }
+ return handler.Invoke(entity, _componentContext);
}
public IEnumerable GetAllCacheControlTags()
diff --git a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
index fe31dd4eac..234b67ced2 100644
--- a/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
+++ b/src/Libraries/SmartStore.Core/Caching/OutputCache/IOutputCacheInvalidationObserver.cs
@@ -1,20 +1,20 @@
using System;
-using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
-using Autofac;
using SmartStore.Core.Configuration;
-using SmartStore.Core.Data;
+using SmartStore.Core.Data.Hooks;
using SmartStore.Core.Infrastructure.DependencyManagement;
+using SmartStore.Utilities;
namespace SmartStore.Core.Caching
{
public class ObserveEntityContext
{
public IOutputCacheProvider OutputCacheProvider { get; set; }
+ public IDisplayControl DisplayControl { get; set; }
public BaseEntity Entity { get; set; }
- public EntityState EntityState { get; set; }
+ public IHookedEntity EntityEntry { get; set; }
public bool Handled { get; set; }
public ContainerManager ServiceContainer { get; set; }
}
@@ -155,7 +155,7 @@ public static void ObserveSettingProperty(
{
Guard.NotNull(propertyAccessor, nameof(propertyAccessor));
- var key = typeof(TSetting).Name + "." + propertyAccessor.ExtractPropertyInfo().Name;
+ var key = TypeHelper.NameOf(propertyAccessor, true);
observer.ObserveSetting(key, invalidationAction);
}
}
diff --git a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs
index fe900f985b..5fb74d2400 100644
--- a/src/Libraries/SmartStore.Core/Caching/RequestCache.cs
+++ b/src/Libraries/SmartStore.Core/Caching/RequestCache.cs
@@ -4,10 +4,11 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Web;
+using SmartStore.Utilities;
namespace SmartStore.Core.Caching
{
- public class RequestCache : IRequestCache
+ public class RequestCache : DisposableObject, IRequestCache
{
const string RegionName = "SmartStoreNET:";
@@ -81,7 +82,7 @@ public void RemoveByPattern(string pattern)
foreach (string key in keysToRemove)
{
- items.Remove(key);
+ items.Remove(BuildKey(key));
}
}
@@ -100,19 +101,20 @@ public IEnumerable Keys(string pattern)
var prefixLen = RegionName.Length;
pattern = pattern.NullEmpty() ?? "*";
+ var wildcard = new Wildcard(pattern, RegexOptions.IgnoreCase);
var enumerator = items.GetEnumerator();
while (enumerator.MoveNext())
{
- string key = enumerator.Key as string;
- if (key == null)
- continue;
- if (key.StartsWith(RegionName))
+ if (enumerator.Key is string key)
{
- key = key.Substring(prefixLen);
- if (pattern == "*" || key.StartsWith(pattern, StringComparison.OrdinalIgnoreCase))
+ if (key.StartsWith(RegionName))
{
- yield return key;
+ key = key.Substring(prefixLen);
+ if (pattern == "*" || wildcard.IsMatch(key))
+ {
+ yield return key;
+ }
}
}
}
@@ -122,5 +124,11 @@ private string BuildKey(string key)
{
return RegionName + key.EmptyNull();
}
+
+ protected override void OnDispose(bool disposing)
+ {
+ if (disposing)
+ Clear();
+ }
}
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
index d9e6c700b6..86d5b00249 100644
--- a/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
+++ b/src/Libraries/SmartStore.Core/Collections/ConcurrentMultimap.cs
@@ -3,8 +3,9 @@
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Linq;
-using System.Linq.Expressions;
using Newtonsoft.Json;
+using SmartStore.Utilities;
+using System.Threading;
namespace SmartStore.Collections
{
@@ -15,40 +16,65 @@ namespace SmartStore.Collections
/// The type of value.
[JsonConverter(typeof(ConcurrentMultiMapConverter))]
[Serializable]
- public class ConcurrentMultimap : IEnumerable>>
+ public class ConcurrentMultimap : IEnumerable>>
{
- private readonly ConcurrentDictionary> _items;
- private static readonly Func, IProducerConsumerCollection> _bagCreator = (col) => new ConcurrentBag(col);
+ private readonly ConcurrentDictionary> _dict;
+ private readonly Func, ICollection> _collectionCreator;
public ConcurrentMultimap()
- : this(null, null)
+ : this(null, null, null)
{
}
public ConcurrentMultimap(IEqualityComparer comparer)
- : this(null, comparer)
+ : this(null, comparer, null)
{
}
- public ConcurrentMultimap(IEnumerable>> items)
- : this(items, null)
+ public ConcurrentMultimap(IEnumerable>> items)
+ : this(items, null, null)
{
// for serialization
}
- public ConcurrentMultimap(IEnumerable>> items, IEqualityComparer comparer)
+ public ConcurrentMultimap(
+ IEnumerable>> items,
+ IEqualityComparer comparer)
+ : this(items, comparer, null)
{
- _items = new ConcurrentDictionary>(comparer ?? EqualityComparer.Default);
+ // for serialization
+ }
+
+ public ConcurrentMultimap(
+ IEnumerable>> items,
+ IEqualityComparer comparer,
+ Func, ICollection> collectionCreator)
+ {
+ _collectionCreator = collectionCreator;
+ _dict = new ConcurrentDictionary>(
+ ConvertItems(items),
+ comparer ?? EqualityComparer.Default);
+ }
+
+ private IEnumerable>> ConvertItems(IEnumerable>> items)
+ {
+ if (items == null)
+ yield break;
- if (items != null)
+ foreach (var item in items)
{
- foreach (var kvp in items)
- {
- _items.TryAdd(kvp.Key, _bagCreator(kvp.Value ?? Enumerable.Empty()));
- }
+ yield return new KeyValuePair>(item.Key, CreateCollection(item.Value));
}
}
+ protected virtual SyncedCollection CreateCollection(IEnumerable values)
+ {
+ var creator = _collectionCreator ?? Multimap.DefaultCollectionCreator;
+ var col = creator(values ?? Enumerable.Empty());
+
+ return col.AsSynchronized();
+ }
+
///
/// Gets the count of groups/keys.
///
@@ -56,7 +82,7 @@ public int Count
{
get
{
- return _items.Keys.Count;
+ return _dict.Keys.Count;
}
}
@@ -67,25 +93,33 @@ public int TotalValueCount
{
get
{
- return this._items.Values.Sum(x => x.Count);
+ return this._dict.Values.Sum(x => x.Count);
}
}
+ ///
+ /// Gets the count of items in the requested group.
+ ///
+ public int ValueCount(TKey key)
+ {
+ if (_dict.TryGetValue(key, out var col))
+ {
+ return col.Count;
+ }
+
+ return 0;
+ }
+
///
/// Gets the collection of values stored under the specified key.
///
/// The key.
- public virtual IProducerConsumerCollection this[TKey key]
+ public virtual SyncedCollection this[TKey key]
{
get
{
- IProducerConsumerCollection value;
- if (!_items.TryGetValue(key, out value))
- {
- _items.TryAdd(key, _bagCreator(Enumerable.Empty()));
- }
-
- return value;
+ GetOrCreateValues(key, null, out var col);
+ return col;
}
}
@@ -94,15 +128,15 @@ public virtual IProducerConsumerCollection this[TKey key]
///
public virtual ICollection Keys
{
- get { return _items.Keys; }
+ get { return _dict.Keys; }
}
///
- /// Gets the collection of collections of values.
+ /// Gets all value collections.
///
- public virtual ICollection> Values
+ public virtual ICollection> Values
{
- get { return _items.Values; }
+ get { return _dict.Values; }
}
///
@@ -112,7 +146,10 @@ public virtual ICollection> Values
/// The value.
public virtual void TryAdd(TKey key, TValue value)
{
- this[key].TryAdd(value);
+ if (!GetOrCreateValues(key, new[] { value }, out var col))
+ {
+ col.Add(value);
+ }
}
///
@@ -124,18 +161,63 @@ public virtual void TryAddRange(TKey key, IEnumerable values)
{
Guard.NotNull(values, nameof(values));
- values.Each(x => this[key].TryAdd(x));
+ if (!GetOrCreateValues(key, values, out var col))
+ {
+ col.AddRange(values);
+ }
+ }
+
+ ///
+ /// Attempts to remove the specified value for the specified key.
+ ///
+ /// The key.
+ /// The value.
+ /// True if such a value existed and was removed; otherwise false .
+ public virtual bool TryRemove(TKey key, TValue value)
+ {
+ if (!_dict.ContainsKey(key))
+ return false;
+
+ var col = _dict[key];
+ var removed = false;
+
+ removed = col.Remove(value);
+ if (col.Count == 0)
+ {
+ _dict.TryRemove(key, out _);
+ }
+
+ return removed;
+ }
+
+ ///
+ /// Attempts to remove a range of values for the specified key.
+ ///
+ /// The key.
+ /// The values to remove from the group.
+ /// True if at least one item in group has been removed; otherwise false .
+ public virtual bool TryRemoveRange(TKey key, IEnumerable values)
+ {
+ Guard.NotNull(values, nameof(values));
+
+ int numRemoved = 0;
+
+ if (_dict.TryGetValue(key, out var col))
+ {
+ numRemoved = col.RemoveRange(values);
+ }
+
+ return numRemoved > 0;
}
///
- /// Attempts to remove all values for the specified key.
+ /// Attempts to remove and return all values for the specified key.
///
/// The key.
/// True if any such values existed; otherwise false .
- public virtual bool TryRemoveAll(TKey key)
+ public virtual bool TryRemoveAll(TKey key, out SyncedCollection collection)
{
- IProducerConsumerCollection collection;
- return _items.TryRemove(key, out collection);
+ return _dict.TryRemove(key, out collection);
}
///
@@ -143,7 +225,7 @@ public virtual bool TryRemoveAll(TKey key)
///
public virtual void Clear()
{
- _items.Clear();
+ _dict.Clear();
}
///
@@ -153,7 +235,7 @@ public virtual void Clear()
/// True if the multimap has one or more values for the specified key, otherwise false .
public virtual bool ContainsKey(TKey key)
{
- return _items.ContainsKey(key);
+ return _dict.ContainsKey(key);
}
///
@@ -164,7 +246,7 @@ public virtual bool ContainsKey(TKey key)
/// True if the multimap contains such a value; otherwise, false .
public virtual bool ContainsValue(TKey key, TValue value)
{
- return _items.ContainsKey(key) && _items[key].Contains(value);
+ return _dict.ContainsKey(key) && _dict[key].Contains(value);
}
///
@@ -180,12 +262,26 @@ IEnumerator IEnumerable.GetEnumerator()
/// Returns an enumerator that iterates through the multimap.
///
/// An object that can be used to iterate through the multimap.
- public virtual IEnumerator>> GetEnumerator()
+ public virtual IEnumerator>> GetEnumerator()
{
- foreach (var pair in _items)
+ foreach (var pair in _dict)
yield return pair;
}
+ private bool GetOrCreateValues(TKey key, IEnumerable initial, out SyncedCollection col)
+ {
+ // Return true when created
+ var created = false;
+
+ col = _dict.GetOrAdd(key, k =>
+ {
+ created = true;
+ return CreateCollection(initial);
+ });
+
+ return created;
+ }
+
#region Static members
public static ConcurrentMultimap CreateFromLookup(ILookup source)
diff --git a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs b/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
deleted file mode 100644
index 6d81d0bc3a..0000000000
--- a/src/Libraries/SmartStore.Core/Collections/MostRecentlyUsedList.cs
+++ /dev/null
@@ -1,101 +0,0 @@
-using System.Collections;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace SmartStore.Collections
-{
- public class MostRecentlyUsedList : IEnumerable
- {
- private readonly int _maxSize;
- private readonly List _mru;
-
- public MostRecentlyUsedList(int maxSize)
- {
- _maxSize = maxSize;
- _mru = new List();
- }
-
- public MostRecentlyUsedList(IEnumerable collection, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.ToList();
-
- Normalize();
- }
-
- public MostRecentlyUsedList(string collection, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().ToList();
-
- Normalize();
- }
-
- public MostRecentlyUsedList(IEnumerable collection, T newItem, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.ToList();
-
- Add(newItem);
- }
-
- public MostRecentlyUsedList(string collection, T newItem, int maxSize)
- {
- _maxSize = maxSize;
- _mru = collection.SplitSafe(Delimiter).Cast().ToList();
-
- Add(newItem);
- }
-
- public static string Delimiter { get { return ";"; } }
-
- private void Normalize()
- {
- if (_maxSize >= 0)
- {
- while (_mru.Count > _maxSize)
- _mru.RemoveAt(_mru.Count - 1);
- }
- }
-
- public IEnumerator GetEnumerator()
- {
- return _mru.GetEnumerator();
- }
-
- IEnumerator IEnumerable.GetEnumerator()
- {
- return _mru.GetEnumerator();
- }
-
- public override string ToString()
- {
- return string.Join(Delimiter, _mru);
- }
-
- public T this[int key]
- {
- get
- {
- return _mru[key];
- }
- set
- {
- _mru[key] = value;
- }
- }
-
- public int Count { get { return _mru.Count; } }
-
- public void Add(T item)
- {
- int i = _mru.IndexOf(item);
- if (i > -1)
- _mru.RemoveAt(i);
-
- _mru.Insert(0, item);
-
- Normalize();
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Collections/MultiMap.cs b/src/Libraries/SmartStore.Core/Collections/MultiMap.cs
index 66221e5096..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,9 +187,12 @@ 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);
+ 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;
}
@@ -300,16 +302,6 @@ public static Multimap CreateFromLookup(ILookup sour
public class MultiMapConverter : JsonConverter
{
- public override bool CanRead
- {
- get { return true; }
- }
-
- public override bool CanWrite
- {
- get { return true; }
- }
-
public override bool CanConvert(Type objectType)
{
var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(Multimap<,>);
diff --git a/src/Libraries/SmartStore.Core/Collections/Querystring.cs b/src/Libraries/SmartStore.Core/Collections/Querystring.cs
index 9df23fc868..ba5a3cc895 100644
--- a/src/Libraries/SmartStore.Core/Collections/Querystring.cs
+++ b/src/Libraries/SmartStore.Core/Collections/Querystring.cs
@@ -1,14 +1,14 @@
-using System.Text;
-using System.Web;
-using System.Collections.Specialized;
+using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
+using System.Text;
+using System.Web;
namespace SmartStore.Collections
{
- ///
- /// http://weblogs.asp.net/bradvincent/archive/2008/10/27/helper-class-querystring-builder-chainable.aspx
- ///
- public class QueryString : NameValueCollection
+ ///
+ /// http://weblogs.asp.net/bradvincent/archive/2008/10/27/helper-class-querystring-builder-chainable.aspx
+ ///
+ public class QueryString : NameValueCollection
{
public QueryString()
{
@@ -19,17 +19,27 @@ public QueryString(string queryString)
FillFromString(queryString);
}
- public static QueryString Current
+ public QueryString(NameValueCollection queryString)
+ : base(queryString)
+ {
+ }
+
+ public static QueryString Current
{
get { return new QueryString().FromCurrent(); }
}
- ///
- /// extracts a querystring from a full URL
- ///
- /// the string to extract the querystring from
- /// a string representing only the querystring
- [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
+ public static QueryString CurrentUnvalidated
+ {
+ get { return new QueryString().FromCurrent(true); }
+ }
+
+ ///
+ /// Extracts a querystring from a full URL
+ ///
+ /// the string to extract the querystring from
+ /// a string representing only the querystring
+ [SuppressMessage("ReSharper", "StringIndexOfIsCultureSpecific.1")]
public static string ExtractQuerystring(string s)
{
if (!string.IsNullOrEmpty(s))
@@ -39,6 +49,7 @@ public static string ExtractQuerystring(string s)
return s.Substring(s.IndexOf("?") + 1);
}
}
+
return s;
}
@@ -71,12 +82,20 @@ public QueryString FillFromString(string s, bool urlDecode = false)
///
/// returns a QueryString object based on the current querystring of the request
///
+ /// true to get values from the unvalidated query string.
/// the QueryString object
- public QueryString FromCurrent()
+ public QueryString FromCurrent(bool unvalidated = false)
{
if (HttpContext.Current != null)
{
- return FillFromString(HttpContext.Current.Request.QueryString.ToString(), true);
+ if (unvalidated)
+ {
+ return FillFromString(HttpContext.Current.Request.Unvalidated.QueryString.ToString(), true);
+ }
+ else
+ {
+ return FillFromString(HttpContext.Current.Request.QueryString.ToString(), true);
+ }
}
base.Clear();
return this;
@@ -94,7 +113,7 @@ public QueryString FromCurrent()
}
///
- /// adds a name value pair to the collection
+ /// Adds a name value pair to the collection
///
/// the name
/// the value associated to the name
@@ -115,15 +134,16 @@ public QueryString Add(string name, string value, bool isUnique)
{
base[name] += "," + HttpUtility.UrlEncode(value);
}
+
return this;
}
- ///
- /// removes a name value pair from the querystring collection
- ///
- /// name of the querystring value to remove
- /// the QueryString object
- public new QueryString Remove(string name)
+ ///
+ /// Removes a name value pair from the querystring collection
+ ///
+ /// name of the querystring value to remove
+ /// the QueryString object
+ public new QueryString Remove(string name)
{
string existingValue = base[name];
if (!string.IsNullOrEmpty(existingValue))
@@ -175,24 +195,46 @@ public bool Contains(string name)
}
///
- /// outputs the querystring object to a string
+ /// Outputs the querystring object to a string
///
/// the encoded querystring as it would appear in a browser
public override string ToString()
{
- var builder = new StringBuilder();
- for (var i = 0; i < base.Keys.Count; i++)
- {
- if (!string.IsNullOrEmpty(base.Keys[i]))
- {
- foreach (string val in base[base.Keys[i]].EmptyNull().Split(','))
- {
- builder.Append((builder.Length == 0) ? "?" : "&").Append(
- HttpUtility.UrlEncode(base.Keys[i])).Append("=").Append(val);
- }
- }
- }
- return builder.ToString();
+ return ToString(true);
}
- }
+
+ ///
+ /// Outputs the querystring object to a string
+ ///
+ /// Whether to create entries for each comma-separated value
+ /// the encoded querystring as it would appear in a browser
+ public string ToString(bool splitValues)
+ {
+ var builder = new StringBuilder();
+ for (var i = 0; i < base.Keys.Count; i++)
+ {
+ var key = base.Keys[i];
+ var value = base[key];
+
+ if (!string.IsNullOrEmpty(key))
+ {
+ builder.Append((builder.Length == 0) ? "?" : "&");
+
+ if (splitValues)
+ {
+ foreach (string val in value.EmptyNull().Split(','))
+ {
+ builder.Append(HttpUtility.UrlEncode(key)).Append("=").Append(val);
+ }
+ }
+ else
+ {
+ builder.Append(HttpUtility.UrlEncode(key)).Append("=").Append(value);
+ }
+ }
+ }
+
+ return builder.ToString();
+ }
+ }
}
diff --git a/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs
new file mode 100644
index 0000000000..b70b4302d2
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/ReferenceEqualityComparer.cs
@@ -0,0 +1,15 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace SmartStore.ComponentModel
+{
+ public sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer
+ {
+ public static ReferenceEqualityComparer Default { get; } = new ReferenceEqualityComparer();
+
+ public new bool Equals(object x, object y) => ReferenceEquals(x, y);
+ public int GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs
new file mode 100644
index 0000000000..d8100effd8
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/SyncedCollection.cs
@@ -0,0 +1,222 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Collections
+{
+ public sealed class SyncedCollection : ICollection
+ {
+ // INFO: Don't call it SynchronizedCollection because of framework dupe.
+ private readonly ICollection _col;
+
+ public SyncedCollection(ICollection wrappedCollection)
+ : this(wrappedCollection, new object())
+ {
+ }
+
+ public SyncedCollection(ICollection wrappedCollection, object syncRoot)
+ {
+ Guard.NotNull(wrappedCollection, nameof(wrappedCollection));
+ Guard.NotNull(syncRoot, nameof(syncRoot));
+
+ _col = wrappedCollection;
+ SyncRoot = syncRoot;
+ }
+
+ public object SyncRoot { get; }
+
+ public bool ReadLockFree { get; set; }
+
+ public void AddRange(IEnumerable collection)
+ {
+ lock (SyncRoot)
+ {
+ _col.AddRange(collection);
+ }
+ }
+
+ public void Insert(int index, T item)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.Insert(index, item);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void InsertRange(int index, IEnumerable values)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.InsertRange(index, values);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public int RemoveRange(IEnumerable values)
+ {
+ int numRemoved = 0;
+
+ lock (SyncRoot)
+ {
+ foreach (var value in values)
+ {
+ if (_col.Remove(value))
+ numRemoved++;
+ }
+ }
+
+ return numRemoved;
+ }
+
+ public void RemoveRange(int index, int count)
+ {
+ if (_col is List list)
+ {
+ lock (SyncRoot)
+ {
+ list.RemoveRange(index, count);
+ }
+ }
+
+ throw new NotSupportedException();
+ }
+
+ public void RemoveAt(int index)
+ {
+ lock (SyncRoot)
+ {
+ if (_col is List list)
+ {
+ list.RemoveAt(index);
+ }
+ else
+ {
+ var item = _col.ElementAtOrDefault(index);
+ if (item != null)
+ {
+ _col.Remove(item);
+ }
+ }
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ if (ReadLockFree)
+ {
+ return _col.ElementAt(index);
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.ElementAt(index);
+ }
+ }
+ }
+ }
+
+ #region ICollection
+
+ public int Count
+ {
+ get
+ {
+ if (ReadLockFree)
+ {
+ return _col.Count();
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.Count();
+ }
+ }
+ }
+ }
+
+ public bool IsReadOnly => _col.IsReadOnly;
+
+ public void Add(T item)
+ {
+ lock (SyncRoot)
+ {
+ _col.Add(item);
+ }
+ }
+
+ public void Clear()
+ {
+ lock (SyncRoot)
+ {
+ _col.Clear();
+ }
+ }
+
+ public bool Contains(T item)
+ {
+ if (ReadLockFree)
+ {
+ return _col.Contains(item);
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.Contains(item);
+ }
+ }
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ lock (SyncRoot)
+ {
+ _col.CopyTo(array, arrayIndex);
+ }
+ }
+
+ public bool Remove(T item)
+ {
+ lock (SyncRoot)
+ {
+ return _col.Remove(item);
+ }
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return this.GetEnumerator();
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ if (ReadLockFree)
+ {
+ return _col.GetEnumerator();
+ }
+ else
+ {
+ lock (SyncRoot)
+ {
+ return _col.GetEnumerator();
+ }
+ }
+ }
+
+ #endregion
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
index be3f19846c..00034d0778 100644
--- a/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
+++ b/src/Libraries/SmartStore.Core/Collections/TreeNode.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
@@ -21,16 +22,22 @@ public TreeNode(TValue value)
}
public TreeNode(TValue value, IEnumerable children)
+ : this(value)
{
- Value = value;
- AppendRange(children);
+ if (children != null && children.Any())
+ {
+ AppendRange(children);
+ }
}
public TreeNode(TValue value, IEnumerable> children)
+ : this(value)
{
// for serialization
- Value = value;
- AppendRange(children);
+ if (children != null && children.Any())
+ {
+ AppendRange(children);
+ }
}
public TValue Value
@@ -48,12 +55,32 @@ protected override TreeNode CreateInstance()
value = ((ICloneable)value).Clone();
}
- return new TreeNode(value);
+ var clonedNode = new TreeNode(value);
+
+ // Assign or clone Metadata
+ if (_metadata != null && _metadata.Count > 0)
+ {
+ foreach (var kvp in _metadata)
+ {
+ var metadataValue = kvp.Value is ICloneable
+ ? ((ICloneable)kvp.Value).Clone()
+ : kvp.Value;
+ clonedNode.SetMetadata(kvp.Key, metadataValue);
+ }
+ }
+
+ if (_id != null)
+ {
+ clonedNode._id = _id;
+ }
+
+ return clonedNode;
}
- public TreeNode Append(TValue value)
+ public TreeNode Append(TValue value, object id = null)
{
var node = new TreeNode(value);
+ node._id = id;
this.Append(node);
return node;
}
@@ -63,9 +90,17 @@ public void AppendRange(IEnumerable values)
values.Each(x => Append(x));
}
- public TreeNode Prepend(TValue value)
+ public void AppendRange(IEnumerable values, Func idSelector)
+ {
+ Guard.NotNull(idSelector, nameof(idSelector));
+
+ values.Each(x => Append(x, idSelector(x)));
+ }
+
+ public TreeNode Prepend(TValue value, object id = null)
{
var node = new TreeNode(value);
+ node._id = id;
this.Prepend(node);
return node;
}
@@ -102,16 +137,6 @@ public IEnumerable Flatten(Expression> expression, bo
public class TreeNodeConverter : JsonConverter
{
- public override bool CanRead
- {
- get { return true; }
- }
-
- public override bool CanWrite
- {
- get { return true; }
- }
-
public override bool CanConvert(Type objectType)
{
var canConvert = objectType.IsGenericType && objectType.GetGenericTypeDefinition() == typeof(TreeNode<>);
@@ -125,6 +150,8 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
object objValue = null;
object objChildren = null;
+ string id = null;
+ Dictionary metadata = null;
reader.Read();
while (reader.TokenType == JsonToken.PropertyName)
@@ -135,37 +162,97 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist
reader.Read();
objValue = serializer.Deserialize(reader, valueType);
}
+ else if (string.Equals(a, "Metadata", StringComparison.OrdinalIgnoreCase))
+ {
+ reader.Read();
+ metadata = serializer.Deserialize>(reader);
+ }
else if (string.Equals(a, "Children", StringComparison.OrdinalIgnoreCase))
{
reader.Read();
objChildren = serializer.Deserialize(reader, sequenceType);
}
+ else if (string.Equals(a, "Id", StringComparison.OrdinalIgnoreCase))
+ {
+ reader.Read();
+ id = serializer.Deserialize(reader);
+ }
else
{
reader.Skip();
}
+
reader.Read();
}
- var treeNode = Activator.CreateInstance(objectType, new object[] { objValue, objChildren });
-
+ var ctorParams = objChildren != null
+ ? new object[] { objValue, objChildren }
+ : new object[] { objValue };
+
+ var treeNode = Activator.CreateInstance(objectType, ctorParams);
+
+ // Set Metadata
+ if (metadata != null && metadata.Count > 0)
+ {
+ var metadataProp = FastProperty.GetProperty(objectType, "Metadata", PropertyCachingStrategy.Cached);
+ metadataProp.SetValue(treeNode, metadata);
+ }
+
+ // Set Id
+ if (id.HasValue())
+ {
+ var idProp = FastProperty.GetProperty(objectType, "Id", PropertyCachingStrategy.Cached);
+ idProp.SetValue(treeNode, id);
+ }
+
return treeNode;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
- var valueProp = FastProperty.GetProperty(value.GetType(), "Value", PropertyCachingStrategy.Cached);
- var childrenProp = FastProperty.GetProperty(value.GetType(), "Children", PropertyCachingStrategy.Cached);
-
writer.WriteStartObject();
{
+ // Id
+ if (GetPropValue("Id", value) is object o)
+ {
+ writer.WritePropertyName("Id");
+ serializer.Serialize(writer, o);
+ }
+
+ // Value
writer.WritePropertyName("Value");
- serializer.Serialize(writer, valueProp.GetValue(value));
+ serializer.Serialize(writer, GetPropValue("Value", value));
- writer.WritePropertyName("Children");
- serializer.Serialize(writer, childrenProp.GetValue(value));
+ // Metadata
+ if (GetPropValue("Metadata", value) is IDictionary dict && dict.Count > 0)
+ {
+ writer.WritePropertyName("Metadata");
+
+ var typeNameHandling = serializer.TypeNameHandling;
+ try
+ {
+ serializer.TypeNameHandling = TypeNameHandling.All;
+ serializer.Serialize(writer, dict);
+ }
+ finally
+ {
+ serializer.TypeNameHandling = typeNameHandling;
+ }
+ }
+
+ // Children
+ if (GetPropValue("HasChildren", value) is bool b && b == true)
+ {
+ writer.WritePropertyName("Children");
+ serializer.Serialize(writer, GetPropValue("Children", value));
+ }
}
writer.WriteEndObject();
}
+
+ private object GetPropValue(string name, object instance)
+ {
+ return FastProperty.GetProperty(instance.GetType(), name, PropertyCachingStrategy.Cached).GetValue(instance);
+ }
}
}
diff --git a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
index 7fa2f214d9..8264db12e1 100644
--- a/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
+++ b/src/Libraries/SmartStore.Core/Collections/TreeNodeBase.cs
@@ -14,13 +14,76 @@ public abstract class TreeNodeBase where T : TreeNodeBase
private int? _depth = null;
private int _index = -1;
- private IDictionary _metadata;
+ protected object _id;
+ private IDictionary> _idNodeMap;
+
+ protected IDictionary _metadata;
private readonly static ContextState> _contextState = new ContextState>("TreeNodeBase.ThreadMetadata");
public TreeNodeBase()
{
}
+ #region Id
+
+ public object Id
+ {
+ get
+ {
+ return _id;
+ }
+ set
+ {
+ _id = value;
+
+ if (_parent != null)
+ {
+ var map = GetIdNodeMap();
+
+ if (_id != null && map.ContainsKey(_id))
+ {
+ // Remove old id from map
+ map.Remove(_id);
+ }
+
+ if (value != null)
+ {
+ map[value] = this;
+ }
+ }
+ }
+ }
+
+ public T SelectNodeById(object id)
+ {
+ if (id == null || IsLeaf)
+ return null;
+
+ var map = GetIdNodeMap();
+ var node = (T)map?.Get(id);
+
+ if (node != null && !this.IsAncestorOfOrSelf(node))
+ {
+ // Found node is NOT a child of this node
+ return null;
+ }
+
+ return node;
+ }
+
+ private IDictionary> GetIdNodeMap()
+ {
+ var map = this.Root._idNodeMap;
+ if (map == null)
+ {
+ map = this.Root._idNodeMap = new Dictionary>();
+ }
+
+ return map;
+ }
+
+ #endregion
+
#region Metadata
public IDictionary Metadata
@@ -76,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)
@@ -137,8 +205,11 @@ private void AttachTo(T newParent, int? index)
{
Guard.NotNull(newParent, nameof(newParent));
+ var prevParent = _parent;
+
if (_parent != null)
{
+ // Detach from parent
_parent.Remove((T)this);
}
@@ -155,6 +226,74 @@ private void AttachTo(T newParent, int? index)
}
_parent = newParent;
+
+ FixIdNodeMap(prevParent, newParent);
+ }
+
+ ///
+ /// Responsible for propagating node ids when detaching/attaching nodes
+ ///
+ private void FixIdNodeMap(T prevParent, T newParent)
+ {
+ ICollection> keyedNodes = null;
+
+ if (prevParent != null)
+ {
+ // A node is moved. We need to detach first.
+ keyedNodes = new List>();
+
+ // Detach ids from prev map
+ var prevMap = prevParent.GetIdNodeMap();
+
+ Traverse(x =>
+ {
+ // Collect all child node's ids
+ if (x._id != null)
+ {
+ keyedNodes.Add(x);
+ if (prevMap.ContainsKey(x._id))
+ {
+ // Remove from map
+ prevMap.Remove(x._id);
+ }
+ }
+ }, true);
+ }
+
+ if (keyedNodes == null && _idNodeMap != null)
+ {
+ // An orphan/root node is attached
+ keyedNodes = _idNodeMap.Values;
+ }
+
+ if (newParent != null)
+ {
+ // Get new *root map
+ var map = newParent.GetIdNodeMap();
+
+ // Merge *this map with *root map
+ if (keyedNodes != null)
+ {
+ foreach (var node in keyedNodes)
+ {
+ map[node._id] = node;
+ }
+
+ // Get rid of *this map after memorizing keyed nodes
+ if (_idNodeMap != null)
+ {
+ _idNodeMap.Clear();
+ _idNodeMap = null;
+ }
+ }
+
+ if (prevParent == null && _id != null)
+ {
+ // When *this was a root, but is keyed, then *this id
+ // was most likely missing in the prev id-node-map.
+ map[_id] = (T)this;
+ }
+ }
}
[JsonIgnore]
@@ -379,11 +518,10 @@ public IEnumerable Trail
var node = (T)this;
do
{
- trail.Add(node);
+ trail.Insert(0, node);
node = node._parent;
} while (node != null);
-
- trail.Reverse();
+
return trail;
}
}
@@ -497,7 +635,7 @@ public T SelectNode(Expression> predicate, bool includeSelf = fals
}
///
- /// Selects all nodes (recursively) with match the given predicate
+ /// Selects all nodes (recursively) witch match the given predicate
///
/// The predicate to match against
/// A readonly collection of node matches
@@ -528,6 +666,8 @@ public void Remove(T node)
var list = node._parent?._children;
if (list.Remove(node))
{
+ node.FixIdNodeMap(node._parent, null);
+
FixIndexes(list, node._index, -1);
node._index = -1;
@@ -544,6 +684,8 @@ public void Clear()
{
_children.Clear();
}
+
+ FixIdNodeMap(_parent, null);
}
public void Traverse(Action action, bool includeSelf = false)
@@ -556,7 +698,7 @@ public void Traverse(Action action, bool includeSelf = false)
if (_children != null)
{
foreach (var child in _children)
- child.Traverse(action);
+ child.Traverse(action, true);
}
}
@@ -576,7 +718,7 @@ public void TraverseParents(Action action, bool includeSelf = false)
}
}
- protected IEnumerable FlattenNodes(bool includeSelf = true)
+ public IEnumerable FlattenNodes(bool includeSelf = true)
{
return this.FlattenNodes(null, includeSelf);
}
diff --git a/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs
new file mode 100644
index 0000000000..db54bf8936
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/Collections/TrimmedBuffer.cs
@@ -0,0 +1,106 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace SmartStore.Collections
+{
+ public sealed class TrimmedBuffer : ICollection
+ {
+ private readonly int _maxSize;
+ private readonly List _list;
+
+ public TrimmedBuffer(int maxSize)
+ : this((IEnumerable)null, default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(IEnumerable collection, int maxSize)
+ : this(collection, default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(string collection, int maxSize)
+ : this(collection.NullEmpty()?.Convert>()?.Distinct(), default(T), maxSize)
+ {
+ }
+
+ public TrimmedBuffer(string collection, T newItem, int maxSize)
+ : this(collection.NullEmpty()?.Convert>()?.Distinct(), newItem, maxSize)
+ {
+ }
+
+ public TrimmedBuffer(IEnumerable collection, T newItem, int maxSize)
+ {
+ Guard.IsTrue(maxSize >= 0, nameof(maxSize));
+
+ _maxSize = maxSize;
+ _list = new List(collection ?? Enumerable.Empty());
+
+ if (newItem != null)
+ {
+ Add(newItem);
+ }
+
+ Trim();
+ }
+
+ private void Trim()
+ {
+ if (_maxSize >= 0)
+ {
+ while (_list.Count > _maxSize)
+ _list.RemoveAt(_list.Count - 1);
+ }
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ return _list[index];
+ }
+ set
+ {
+ _list[index] = value;
+ }
+ }
+
+ public void Add(T item)
+ {
+ var i = _list.IndexOf(item);
+ if (i > -1)
+ _list.RemoveAt(i);
+
+ _list.Insert(0, item);
+
+ Trim();
+ }
+
+ public void Clear() => _list.Clear();
+
+ public bool Contains(T item) => _list.Contains(item);
+
+ public void CopyTo(T[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex);
+
+ public bool Remove(T item) => _list.Remove(item);
+
+ public int Count => _list.Count;
+
+ public bool IsReadOnly => false;
+
+ public IEnumerator GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ public override string ToString()
+ {
+ return string.Join(";", _list);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
index d37ec07015..85419958d3 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastActivator.cs
@@ -44,6 +44,8 @@ public object Activate(params object[] parameters)
return Invoker(parameters);
}
+ #region Static
+
///
/// Creates a single fast constructor invoker. The result is not cached.
///
@@ -78,8 +80,6 @@ public static Func MakeFastInvoker(ConstructorInfo constructor
return lambda.Compile();
}
- #region Static
-
///
/// Creates and caches fast constructor invokers
///
@@ -93,8 +93,7 @@ public static FastActivator[] GetActivators(Type type)
private static FastActivator[] GetActivatorsCore(Type type)
{
- FastActivator[] activators;
- if (!_activatorsCache.TryGetValue(type, out activators))
+ if (!_activatorsCache.TryGetValue(type, out FastActivator[] activators))
{
var candidates = GetCandidateConstructors(type);
activators = candidates.Select(c => new FastActivator(c)).ToArray();
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
new file mode 100644
index 0000000000..ba43358e1a
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastInvoker.cs
@@ -0,0 +1,293 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Linq.Expressions;
+using System.Collections.Concurrent;
+using SmartStore.Utilities;
+
+namespace SmartStore.ComponentModel
+{
+ public class FastInvoker
+ {
+ private static readonly ConcurrentDictionary _invokersCache = new ConcurrentDictionary();
+
+ public FastInvoker(MethodInfo methodInfo)
+ {
+ Guard.NotNull(methodInfo, nameof(methodInfo));
+
+ Method = methodInfo;
+ Invoker = MakeFastInvoker(methodInfo);
+ ParameterTypes = methodInfo.GetParameters().Select(p => p.ParameterType).ToArray();
+ }
+
+ ///
+ /// Gets the backing .
+ ///
+ public MethodInfo Method { get; private set; }
+
+ ///
+ /// Gets the parameter types from the backing
+ ///
+ public Type[] ParameterTypes { get; private set; }
+
+ ///
+ /// Gets the method invoker.
+ ///
+ public Func Invoker { get; private set; }
+
+ ///
+ /// Invokes the method using the specified parameters.
+ ///
+ /// The method invocation result.
+ public object Invoke(object obj, params object[] parameters)
+ {
+ return Invoker(obj, parameters);
+ }
+
+ #region Static
+
+ ///
+ /// Creates a single fast method invoker. The result is not cached.
+ ///
+ /// Method to create invoker for.
+ /// The fast method invoker delegate.
+ public static Func MakeFastInvoker(MethodInfo method)
+ {
+ Guard.NotNull(method, nameof(method));
+
+ var instanceParameterExpression = Expression.Parameter(typeof(object), "instance");
+ var argumentsParameterExpression = Expression.Parameter(typeof(object[]), "args");
+ var index = 0;
+
+ var argumentExtractionExpressions =
+ method
+ .GetParameters()
+ .Select(parameter =>
+ Expression.Convert(
+ Expression.ArrayAccess(
+ argumentsParameterExpression,
+ Expression.Constant(index++)
+ ),
+ parameter.ParameterType
+ )
+ ).ToList();
+
+ var callExpression = method.IsStatic
+ ? Expression.Call(method, argumentExtractionExpressions)
+ : Expression.Call(
+ Expression.Convert(
+ instanceParameterExpression,
+ method.DeclaringType
+ ),
+ method,
+ argumentExtractionExpressions
+ );
+
+ var endLabel = Expression.Label(typeof(object));
+
+ var finalExpression = method.ReturnType == typeof(void)
+ ? (Expression)Expression.Block(
+ callExpression,
+ Expression.Return(endLabel, Expression.Constant(null)),
+ Expression.Label(endLabel, Expression.Constant(null))
+ )
+ : Expression.Convert(callExpression, typeof(object));
+
+ var lambdaExpression = Expression.Lambda>(
+ finalExpression,
+ instanceParameterExpression,
+ argumentsParameterExpression
+ );
+
+ var lamdba = lambdaExpression.Compile();
+ return lamdba;
+ }
+
+ ///
+ /// Invokes a method using the specified object and parameter instances.
+ ///
+ /// The objectinstance
+ /// Method name
+ /// Argument types of the matching method overload (in exact order)
+ /// Parameter instances to pass to invocation
+ /// The method invocation result.
+ public static object Invoke(object obj, string methodName, Type[] parameterTypes, object[] parameters)
+ {
+ Guard.NotNull(obj, nameof(obj));
+
+ FastInvoker invoker;
+
+ if (parameterTypes == null || parameterTypes.Length == 0)
+ {
+ invoker = GetInvoker(obj.GetType(), methodName);
+ }
+ else
+ {
+ invoker = GetInvoker(obj.GetType(), methodName, parameterTypes);
+ }
+
+ //var hasAnyNullParam = parameters.Any(x => x == null);
+ //if (hasAnyNullParam)
+ //{
+ // throw new ArgumentException("When invoking a method with parameter instances, no instance must be null.", nameof(parameters));
+ //}
+
+ return invoker.Invoke(obj, parameters ?? Array.Empty());
+ }
+
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// Name of method to create an invoker for.
+ /// Argument types of method to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(string methodName, params Type[] argTypes)
+ {
+ return GetInvoker(typeof(T), methodName, argTypes);
+ }
+
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// The type to extract fast method invoker for.
+ /// Name of method to create an invoker for.
+ /// Argument types of method to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(Type type, string methodName, params Type[] argTypes)
+ {
+ Guard.NotNull(type, nameof(type));
+ Guard.NotEmpty(methodName, nameof(methodName));
+
+ var cacheKey = MethodKey.Create(type, methodName, argTypes);
+
+ if (!_invokersCache.TryGetValue(cacheKey, out var invoker))
+ {
+ var method = FindMatchingMethod(type, methodName, argTypes);
+
+ if (method == null)
+ {
+ throw new MethodAccessException("Could not find a matching method '{0}' in type {1}.".FormatInvariant(methodName, type));
+ }
+
+ invoker = new FastInvoker(method);
+ _invokersCache.TryAdd(cacheKey, invoker);
+ }
+
+ return invoker;
+ }
+
+ ///
+ /// Creates and caches a fast method invoker.
+ ///
+ /// Method info instance to create an invoker for.
+ /// The fast method invoker.
+ public static FastInvoker GetInvoker(MethodInfo method)
+ {
+ Guard.NotNull(method, nameof(method));
+
+ var cacheKey = MethodKey.Create(method);
+
+ if (!_invokersCache.TryGetValue(cacheKey, out var invoker))
+ {
+ invoker = new FastInvoker(method);
+ _invokersCache.TryAdd(cacheKey, invoker);
+ }
+
+ return invoker;
+ }
+
+ private static MethodInfo FindMatchingMethod(Type type, string methodName, Type[] argTypes)
+ {
+ var method = argTypes == null || argTypes.Length == 0
+ ? type.GetMethod(methodName)
+ : type.GetMethod(
+ methodName,
+ BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly,
+ null,
+ argTypes ?? new Type[0],
+ null);
+
+ return method;
+ }
+
+ #endregion
+
+ abstract class MethodKey
+ {
+ public override bool Equals(object obj) =>
+ throw new NotImplementedException();
+
+ public override int GetHashCode() =>
+ throw new NotImplementedException();
+
+ public static bool operator ==(MethodKey left, MethodKey right) =>
+ object.Equals(left, right);
+
+ public static bool operator !=(MethodKey left, MethodKey right) =>
+ !(left == right);
+
+ internal static MethodKey Create(Type type, string methodName, IEnumerable parameterTypes)
+ {
+ return new HashMethodKey(type, methodName, parameterTypes);
+ }
+
+ internal static MethodKey Create(MethodInfo method)
+ {
+ return new MethodInfoKey(method);
+ }
+ }
+
+ class HashMethodKey : MethodKey, IEquatable
+ {
+ private readonly int _hash;
+
+ public HashMethodKey(Type type, string methodName, IEnumerable parameterTypes)
+ {
+ _hash = HashCodeCombiner.Start().Add(type).Add(methodName).Add(parameterTypes).CombinedHash;
+ }
+
+ public override bool Equals(object obj) =>
+ this.Equals(obj as HashMethodKey);
+
+ public bool Equals(HashMethodKey other)
+ {
+ if (other == null)
+ return false;
+
+ return this._hash == other._hash;
+ }
+
+ public override int GetHashCode()
+ {
+ return _hash;
+ }
+ }
+
+ class MethodInfoKey : MethodKey, IEquatable
+ {
+ private readonly MethodInfo _method;
+
+ public MethodInfoKey(MethodInfo method)
+ {
+ _method = method;
+ }
+
+ public override bool Equals(object obj) =>
+ this.Equals(obj as MethodInfoKey);
+
+ public bool Equals(MethodInfoKey other)
+ {
+ if (other == null)
+ return false;
+
+ return this._method == other._method;
+ }
+
+ public override int GetHashCode()
+ {
+ return _method.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs
index a4865684cf..7aafa809cd 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/FastProperty.cs
@@ -146,7 +146,7 @@ public void SetValue(object instance, object value)
/// the instance to extract property accessors for.
/// A cached array of all public property getters from the underlying type of target instance.
///
- public static IReadOnlyDictionary GetProperties(object instance, PropertyCachingStrategy cachingStrategy = PropertyCachingStrategy.Cached)
+ public static IReadOnlyDictionary GetProperties(object instance)
{
return GetProperties(instance.GetType());
}
@@ -224,9 +224,7 @@ public static FastProperty GetProperty(
Guard.NotNull(type, nameof(type));
Guard.NotEmpty(propertyName, nameof(propertyName));
- FastProperty fastProperty = null;
-
- if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out fastProperty))
+ if (TryGetCachedProperty(type, propertyName, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty))
{
return fastProperty;
}
@@ -254,9 +252,7 @@ public static FastProperty GetProperty(
{
Guard.NotNull(propertyInfo, nameof(propertyInfo));
- FastProperty fastProperty = null;
-
- if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out fastProperty))
+ if (TryGetCachedProperty(propertyInfo.ReflectedType, propertyInfo.Name, cachingStrategy == PropertyCachingStrategy.EagerCached, out var fastProperty))
{
return fastProperty;
}
@@ -446,27 +442,34 @@ public static Action MakeFastPropertySetter(PropertyInfo propert
/// instance, then a copy
/// is returned.
///
+ /// Key selector
+ /// When true, converts all nested objects to dictionaries also
///
/// The implementation of FastProperty will cache the property accessors per-type. This is
/// faster when the the same type is used multiple times with ObjectToDictionary.
///
- public static IDictionary ObjectToDictionary(object value, Func keySelector = null)
+ public static IDictionary ObjectToDictionary(object value, Func keySelector = null, bool deep = false)
{
- var dictionary = value as IDictionary;
- if (dictionary != null)
+ if (value is IDictionary dictionary)
{
return new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase);
}
- keySelector = keySelector ?? new Func(key => key);
-
dictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
if (value != null)
{
+ keySelector = keySelector ?? new Func(key => key);
+
foreach (var prop in GetProperties(value).Values)
{
- dictionary[keySelector(prop.Name)] = prop.GetValue(value);
+ var propValue = prop.GetValue(value);
+ if (deep && propValue != null && prop.Property.PropertyType.IsPlainObjectType())
+ {
+ propValue = ObjectToDictionary(propValue, deep: true);
+ }
+
+ dictionary[keySelector(prop.Name)] = propValue;
}
}
@@ -536,12 +539,11 @@ protected static IDictionary GetVisibleProperties(
ConcurrentDictionary> allPropertiesCache,
ConcurrentDictionary> visiblePropertiesCache)
{
- IDictionary result;
- if (visiblePropertiesCache.TryGetValue(type, out result))
+ if (visiblePropertiesCache.TryGetValue(type, out var result))
{
return result;
}
-
+
// The simple and common case, this is normal POCO object - no need to allocate.
var allPropertiesDefinedOnType = true;
var allProperties = GetProperties(type, createPropertyHelper, allPropertiesCache);
@@ -617,18 +619,17 @@ protected static IDictionary GetProperties(
// part of the sequence of properties returned by this method.
type = Nullable.GetUnderlyingType(type) ?? type;
- IDictionary fastProperties;
- if (!cache.TryGetValue(type, out fastProperties))
+ return cache.GetOrAdd(type, Get);
+
+ IDictionary Get(Type t)
{
- var candidates = GetCandidateProperties(type);
- fastProperties = candidates.Select(p => createPropertyHelper(p)).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
- cache.TryAdd(type, fastProperties);
+ var candidates = GetCandidateProperties(t);
+ var fastProperties = candidates.Select(p => createPropertyHelper(p)).ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
+ return fastProperties;
}
-
- return fastProperties;
}
- private static IEnumerable GetCandidateProperties(Type type)
+ internal static IEnumerable GetCandidateProperties(Type type)
{
// We avoid loading indexed properties using the Where statement.
var properties = type.GetRuntimeProperties().Where(IsCandidateProperty);
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs
index 1049481e35..17d44dbbac 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/HybridExpando.cs
@@ -37,10 +37,17 @@
using System.Dynamic;
using System.Reflection;
using System.Collections;
+using SmartStore.Utilities;
namespace SmartStore.ComponentModel
{
- ///
+ public enum MemberOptMethod
+ {
+ Allow,
+ Disallow
+ }
+
+ ///
/// Class that provides extensible properties and methods to an
/// existing object when cast to dynamic. This
/// dynamic object stores 'extra' properties in a dictionary or
@@ -71,22 +78,33 @@ public class HybridExpando : DynamicObject, IDictionary
///
private Type _instanceType;
- ///
- /// String Dictionary that contains the extra dynamic values
- /// stored on this object/instance
- ///
- /// Using PropertyBag to support XML Serialization of the dictionary
- public PropertyBag Properties = new PropertyBag();
+ ///
+ /// Adjusted property list for the wrapped instance type after white/black-list members has been applied.
+ ///
+ private IDictionary _instanceProps;
+ private readonly static IDictionary EmptyProps = new Dictionary();
- ///
- /// This constructor just works off the internal dictionary and any
- /// public properties of this object.
- ///
- /// Note you can subclass Expando.
- ///
- public HybridExpando()
+ ///
+ /// String Dictionary that contains the extra dynamic values
+ /// stored on this object/instance
+ ///
+ /// Using PropertyBag to support XML Serialization of the dictionary
+ public PropertyBag Properties = new PropertyBag();
+
+ private readonly HashSet _optMembers;
+ private readonly MemberOptMethod _optMethod;
+
+ private readonly bool _returnNullWhenFalsy;
+
+ ///
+ /// This constructor just works off the internal dictionary and any
+ /// public properties of this object.
+ ///
+ /// Note you can subclass HybridExpando.
+ ///
+ public HybridExpando(bool returnNullWhenFalsy = false)
{
- Initialize(this);
+ _returnNullWhenFalsy = returnNullWhenFalsy;
}
///
@@ -97,16 +115,42 @@ public HybridExpando()
/// check native properties and only check the Dictionary!
///
///
- public HybridExpando(object instance)
+ public HybridExpando(object instance, bool returnNullWhenFalsy = false)
{
- Initialize(instance);
+ Guard.NotNull(instance, nameof(instance));
+
+ _returnNullWhenFalsy = returnNullWhenFalsy;
+ Initialize(instance);
}
-
- protected void Initialize(object instance)
+
+ ///
+ /// Allows passing in an existing instance variable to 'extend'
+ /// along with a list of member names to allow or disallow.
+ ///
+ ///
+ public HybridExpando(object instance, IEnumerable optMembers, MemberOptMethod optMethod, bool returnNullWhenFalsy = false)
+ {
+ Guard.NotNull(instance, nameof(instance));
+
+ _returnNullWhenFalsy = returnNullWhenFalsy;
+ Initialize(instance);
+
+ _optMethod = optMethod;
+
+ if (optMembers is HashSet h)
+ {
+ _optMembers = h;
+ }
+ else
+ {
+ _optMembers = new HashSet(optMembers);
+ }
+ }
+
+ protected void Initialize(object instance)
{
_instance = instance;
- if (instance != null)
- _instanceType = instance.GetType();
+ _instanceType = instance?.GetType();
}
protected object WrappedObject
@@ -116,19 +160,29 @@ protected object WrappedObject
public override IEnumerable GetDynamicMemberNames()
{
- foreach (var prop in this.GetProperties(false))
- yield return prop.Key;
- }
+ foreach (var kvp in this.Properties.Keys)
+ {
+ yield return kvp;
+ }
+
+ foreach (var kvp in GetInstanceProperties())
+ {
+ if (!this.Properties.ContainsKey(kvp.Key))
+ {
+ yield return kvp.Key;
+ }
+ }
+ }
- ///
- /// Try to retrieve a member by name first from instance properties
- /// followed by the collection entries.
- ///
- ///
- ///
- ///
- public override bool TryGetMember(GetMemberBinder binder, out object result)
+ ///
+ /// Try to retrieve a member by name first from instance properties
+ /// followed by the collection entries.
+ ///
+ ///
+ ///
+ ///
+ public override bool TryGetMember(GetMemberBinder binder, out object result)
{
return TryGetMemberCore(binder.Name, out result);
}
@@ -136,27 +190,33 @@ public override bool TryGetMember(GetMemberBinder binder, out object result)
protected virtual bool TryGetMemberCore(string name, out object result)
{
result = null;
+ bool exists = false;
// first check the Properties collection for member
- if (Properties.Keys.Contains(name))
+ if (Properties.ContainsKey(name))
{
result = Properties[name];
- return true;
+ exists = true;
}
// Next check for public properties via Reflection
- if (_instance != null)
+ if (!exists && _instance != null)
{
try
{
- return GetProperty(_instance, name, out result);
+ exists = GetProperty(_instance, name, out result);
}
catch { }
}
+ // Falsy check
+ if (_returnNullWhenFalsy && result != null && !CommonHelper.IsTruthy(result))
+ {
+ result = null;
+ }
+
// failed to retrieve a property
- result = null;
- return false;
+ return exists;
}
@@ -174,7 +234,14 @@ public override bool TrySetMember(SetMemberBinder binder, object value)
protected virtual bool TrySetMemberCore(string name, object value)
{
- // first check to see if there's a native property to set
+ // first check to see if there's a dictionary entry to set
+ if (Properties.ContainsKey(name))
+ {
+ Properties[name] = value;
+ return true;
+ }
+
+ // Check to see if there's a native property to set
if (_instance != null)
{
try
@@ -198,6 +265,19 @@ protected virtual bool TrySetMemberCore(string name, object value)
///
///
///
+ public void Override(string name, object value = null)
+ {
+ Guard.NotEmpty(name, nameof(name));
+ Properties[name] = value;
+ }
+
+ ///
+ /// Dynamic invocation method. Currently allows only for Reflection based
+ /// operation (no ability to add methods dynamically).
+ ///
+ ///
+ ///
+ ///
///
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
@@ -226,8 +306,7 @@ public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, o
///
protected bool GetProperty(object instance, string name, out object result)
{
- var fastProp = _instanceType != null ? FastProperty.GetProperty(_instanceType, name, PropertyCachingStrategy.EagerCached) : null;
- if (fastProp != null)
+ if (GetInstanceProperties().TryGetValue(name, out var fastProp))
{
result = fastProp.GetValue(instance ?? this);
return true;
@@ -246,12 +325,11 @@ protected bool GetProperty(object instance, string name, out object result)
///
protected bool SetProperty(object instance, string name, object value)
{
- var fastProp = _instanceType != null ? FastProperty.GetProperty(_instanceType, name, PropertyCachingStrategy.EagerCached) : null;
- if (fastProp != null)
+ if (GetInstanceProperties().TryGetValue(name, out var fastProp))
{
fastProp.SetValue(instance ?? this, value);
return true;
- }
+ }
return false;
}
@@ -267,7 +345,7 @@ protected bool SetProperty(object instance, string name, object value)
protected bool InvokeMethod(object instance, string name, object[] args, out object result)
{
// Look at the instanceType
- var mi = _instanceType != null ? _instanceType.GetMethod(name, BindingFlags.Instance | BindingFlags.Public) : null;
+ var mi = _instanceType?.GetMethod(name, BindingFlags.Instance | BindingFlags.Public);
if (mi != null)
{
result = mi.Invoke(instance ?? this, args);
@@ -302,8 +380,7 @@ public object this[string key]
{
get
{
- object result = null;
- if (!TryGetMemberCore(key, out result))
+ if (!TryGetMemberCore(key, out var result))
{
throw new KeyNotFoundException();
}
@@ -324,14 +401,14 @@ public object this[string key]
///
public IEnumerable> GetProperties(bool includeInstanceProperties = false)
{
- foreach (var key in this.Properties.Keys)
+ foreach (var kvp in this.Properties)
{
- yield return new KeyValuePair(key, this.Properties[key]);
+ yield return kvp;
}
- if (includeInstanceProperties && _instance != null)
+ if (includeInstanceProperties)
{
- foreach (var prop in FastProperty.GetProperties(_instance).Values)
+ foreach (var prop in GetInstanceProperties().Values)
{
if (!this.Properties.ContainsKey(prop.Name))
{
@@ -339,9 +416,32 @@ public IEnumerable> GetProperties(bool includeInsta
}
}
}
-
}
+ private IDictionary GetInstanceProperties()
+ {
+ if (_instance == null)
+ {
+ return EmptyProps;
+ }
+
+ if (_instanceProps == null)
+ {
+ var props = FastProperty.GetProperties(_instance) as IDictionary;
+
+ if (_optMembers != null)
+ {
+ props = props
+ .Where(x => _optMethod == MemberOptMethod.Allow ? _optMembers.Contains(x.Key) : !_optMembers.Contains(x.Key))
+ .ToDictionary(x => x.Key, x => x.Value);
+ }
+
+ _instanceProps = props;
+ }
+
+ return _instanceProps;
+ }
+
///
/// Checks whether a property exists in the Property collection
/// or as a property on the instance
@@ -367,10 +467,10 @@ public bool Contains(string propertyName, bool includeInstanceProperties = false
return true;
}
- if (includeInstanceProperties && _instance != null)
+ if (includeInstanceProperties)
{
- return FastProperty.GetProperties(_instance).ContainsKey(propertyName);
- }
+ return GetInstanceProperties().ContainsKey(propertyName);
+ }
return false;
}
@@ -397,15 +497,9 @@ int ICollection>.Count
{
get
{
- var count = Properties.Count;
- if (_instanceType != null)
- {
- count += FastProperty.GetProperties(_instanceType).Count;
+ return GetDynamicMemberNames().Count();
}
-
- return count;
}
- }
bool ICollection>.IsReadOnly
{
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs b/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs
new file mode 100644
index 0000000000..40e6412f8f
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/ComponentModel/MiniMapper.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Reflection;
+using System.Globalization;
+using SmartStore.Utilities;
+
+namespace SmartStore.ComponentModel
+{
+ ///
+ /// A very simple object mapper utility which tries to map properties of the same name between two objects.
+ /// If matched properties has different types, the mapper tries to convert them.
+ /// If conversion fails, the property is skipped (no exception is thrown).
+ /// MiniMapper cannot handle sequence and predefined types.
+ ///
+ public static class MiniMapper
+ {
+ public static TTo Map(TFrom from, CultureInfo culture = null)
+ where TFrom : class
+ where TTo : class
+ {
+ var to = Map(from, typeof(TTo), culture);
+ return (TTo)to;
+ }
+
+ //public static void Map(TFrom from, TTo to, CultureInfo culture = null)
+ // where TFrom : class
+ // where TTo : class
+ //{
+ // Map((object)from, (object)to, culture);
+ //}
+
+ public static object Map(TFrom from, Type toType, CultureInfo culture = null)
+ where TFrom : class
+ {
+ Guard.NotNull(toType, nameof(toType));
+ Guard.HasDefaultConstructor(toType);
+
+ var target = Activator.CreateInstance(toType);
+
+ Map(from, target, culture);
+ return target;
+ }
+
+ public static void Map(TFrom from, TTo to, CultureInfo culture = null)
+ where TFrom : class
+ where TTo : class
+ {
+ Guard.NotNull(from, nameof(from));
+ Guard.NotNull(to, nameof(to));
+
+ if (object.ReferenceEquals(from, to))
+ {
+ // Cannot map the same instance
+ return;
+ }
+
+ var fromType = from.GetType();
+ var toType = to.GetType();
+
+ ValidateType(fromType);
+ ValidateType(toType);
+
+ if (culture == null)
+ {
+ culture = CultureInfo.CurrentCulture;
+ }
+
+ var toProps = GetFastPropertiesFor(toType).ToArray();
+
+ foreach (var toProp in toProps)
+ {
+ var fromProp = FastProperty.GetProperty(fromType, toProp.Name, PropertyCachingStrategy.Uncached);
+ if (fromProp == null)
+ {
+ continue;
+ }
+
+ object value = null;
+ try
+ {
+ // Get the value from source instance and try to convert it to target props type
+ value = fromProp.GetValue(from).Convert(toProp.Property.PropertyType, culture);
+
+ // Set it
+ toProp.SetValue(to, value);
+ }
+ catch { }
+ }
+ }
+
+ private static IEnumerable GetFastPropertiesFor(Type type)
+ {
+ return FastProperty.GetCandidateProperties(type)
+ .Select(pi => FastProperty.GetProperty(pi, PropertyCachingStrategy.Uncached))
+ .Where(pi => pi.IsPublicSettable);
+ }
+
+ private static void ValidateType(Type type)
+ {
+ if (type.IsPredefinedType())
+ {
+ throw new InvalidOperationException("Mapping from or to predefined types is not possible. Type was: {0}".FormatInvariant(type.FullName));
+ }
+
+ if (type.IsSequenceType())
+ {
+ throw new InvalidOperationException("Mapping from or to sequence types is not possible. Type was: {0}".FormatInvariant(type.FullName));
+ }
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs
index bec72dcea5..328ef76ce0 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/PropertyBag.cs
@@ -288,8 +288,7 @@ public void ReadXml(System.Xml.XmlReader reader)
/// XML String or Null if it fails
public string ToXml()
{
- string xml = null;
- SerializationUtils.SerializeObject(this, out xml);
+ SerializationUtils.SerializeObject(this, out var xml);
return xml;
}
@@ -306,8 +305,7 @@ public bool FromXml(string xml)
if (string.IsNullOrEmpty(xml))
return true;
- var result = SerializationUtils.DeSerializeObject(xml, this.GetType()) as PropertyBag;
- if (result != null)
+ if (SerializationUtils.DeSerializeObject(xml, this.GetType()) is PropertyBag result)
{
foreach (var item in result)
{
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs
new file mode 100644
index 0000000000..a7e3c60887
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/DictionaryConverter.cs
@@ -0,0 +1,64 @@
+using System;
+using System.Collections.Generic;
+using System.Dynamic;
+using System.Globalization;
+using System.Web.Routing;
+using SmartStore.Utilities;
+
+namespace SmartStore.ComponentModel
+{
+ public class DictionaryTypeConverter : TypeConverterBase where T : IDictionary
+ {
+ public DictionaryTypeConverter()
+ : base(typeof(object))
+ {
+ }
+
+ public override bool CanConvertFrom(Type type)
+ {
+ return !type.IsPredefinedType() && type.IsClass;
+ }
+
+ public override bool CanConvertTo(Type type)
+ {
+ return !type.IsPredefinedType() && DictionaryConverter.CanCreateType(type);
+ }
+
+ public override object ConvertFrom(CultureInfo culture, object value)
+ {
+ // Obj > Dict
+ var dict = CommonHelper.ObjectToDictionary(value);
+ var to = typeof(T);
+
+ if (to == typeof(RouteValueDictionary))
+ {
+ return new RouteValueDictionary(dict);
+ }
+ if (to == typeof(Dictionary))
+ {
+ return (Dictionary)dict;
+ }
+ else if (to == typeof(ExpandoObject))
+ {
+ var expando = new ExpandoObject();
+ expando.Merge(dict);
+ return expando;
+ }
+ else
+ {
+ return dict;
+ }
+ }
+
+ public override object ConvertTo(CultureInfo culture, string format, object value, Type to)
+ {
+ // Dict > Obj
+ if (value is IDictionary dict)
+ {
+ return DictionaryConverter.CreateAndPopulate(to, dict, out var problems);
+ }
+
+ return base.ConvertTo(culture, format, value, to);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs
new file mode 100644
index 0000000000..5a2c45755b
--- /dev/null
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EmailAddressConverter.cs
@@ -0,0 +1,43 @@
+using System;
+using System.Globalization;
+using SmartStore.Core.Email;
+
+namespace SmartStore.ComponentModel
+{
+ public class EmailAddressConverter : TypeConverterBase
+ {
+ public EmailAddressConverter() : base(typeof(object))
+ {
+ }
+
+ public override bool CanConvertFrom(Type type)
+ {
+ return type == typeof(string);
+ }
+
+ public override bool CanConvertTo(Type type)
+ {
+ return type == typeof(string);
+ }
+
+ public override object ConvertFrom(CultureInfo culture, object value)
+ {
+ if (value is string str && str.HasValue())
+ {
+ return new EmailAddress(str);
+ }
+
+ return base.ConvertFrom(culture, value);
+ }
+
+ public override object ConvertTo(CultureInfo culture, string format, object value, Type to)
+ {
+ if (to == typeof(string) && value is EmailAddress address)
+ {
+ return address.ToString();
+ }
+
+ return base.ConvertTo(culture, format, value, to);
+ }
+ }
+}
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
index 974b7cc973..ae60b58488 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/EnumerableConverter.cs
@@ -122,11 +122,10 @@ public override object ConvertTo(CultureInfo culture, string format, object valu
if (to == typeof(string))
{
string result = string.Empty;
- var enumerable = value as IEnumerable;
- if (enumerable != null)
+ if (value is IEnumerable enumerable)
{
- // we don't use string.Join() because it doesn't support invariant culture
+ // We don't use string.Join() because it doesn't support invariant culture
foreach (var token in enumerable)
{
var str = _elementTypeConverter.ConvertTo(culture, format, token, typeof(string));
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs
index d11c53409f..8488e2aa3e 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/NullableConverter.cs
@@ -68,7 +68,7 @@ public override bool CanConvertFrom(Type type)
public override bool CanConvertTo(Type type)
{
- Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type)));
+ //Console.WriteLine("NullableConverter can convert to {0}: {1}".FormatInvariant(type.Name, UnderlyingTypeConverter.CanConvertTo(type)));
if (type == this.UnderlyingType)
{
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs
index b79c9a5f2f..54df72e96b 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/ShippingOptionConverter.cs
@@ -32,10 +32,9 @@ public override bool CanConvertTo(Type type)
public override object ConvertFrom(CultureInfo culture, object value)
{
- if (value is string)
+ if (value is string str)
{
object result = null;
- string str = value as string;
if (!String.IsNullOrEmpty(str))
{
try
diff --git a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs
index b9f0f504a4..ee6c50e474 100644
--- a/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs
+++ b/src/Libraries/SmartStore.Core/ComponentModel/TypeConversion/TypeConverterFactory.cs
@@ -2,8 +2,11 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
+using System.Dynamic;
+using System.Web.Routing;
using SmartStore.Core.Domain.Catalog;
using SmartStore.Core.Domain.Shipping;
+using SmartStore.Core.Email;
namespace SmartStore.ComponentModel
{
@@ -33,6 +36,14 @@ private static void CreateDefaultConverters()
_typeConverters.TryAdd(typeof(IList), converter);
_typeConverters.TryAdd(typeof(List), converter);
_typeConverters.TryAdd(typeof(ProductBundleItemOrderData), new ProductBundleDataConverter(false));
+
+ converter = new DictionaryTypeConverter>();
+ _typeConverters.TryAdd(typeof(IDictionary), converter);
+ _typeConverters.TryAdd(typeof(Dictionary), converter);
+ _typeConverters.TryAdd(typeof(RouteValueDictionary), new DictionaryTypeConverter());
+ _typeConverters.TryAdd(typeof(ExpandoObject), new DictionaryTypeConverter());
+
+ _typeConverters.TryAdd(typeof(EmailAddress), new EmailAddressConverter());
}
public static void RegisterConverter(ITypeConverter typeConverter)
@@ -57,8 +68,7 @@ public static ITypeConverter RemoveConverter(Type type)
{
Guard.NotNull(type, nameof(type));
- ITypeConverter converter = null;
- _typeConverters.TryRemove(type, out converter);
+ _typeConverters.TryRemove(type, out var converter);
return converter;
}
@@ -76,10 +86,10 @@ public static ITypeConverter GetConverter(object component)
public static ITypeConverter GetConverter(Type type)
{
- Guard.NotNull(type, nameof(type));
+ if (type == null)
+ throw new ArgumentNullException(nameof(type));
- ITypeConverter converter;
- if (_typeConverters.TryGetValue(type, out converter))
+ if (_typeConverters.TryGetValue(type, out var converter))
{
return converter;
}
diff --git a/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs b/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs
index e2c1337143..2f5f65575c 100644
--- a/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs
+++ b/src/Libraries/SmartStore.Core/Configuration/ISettingService.cs
@@ -71,15 +71,22 @@ bool SettingExists(T settings,
/// Store identifier for which settigns should be loaded
T LoadSetting(int storeId = 0) where T : ISettings, new();
- ///
- /// Set setting value
- ///
- /// Type
- /// Key
- /// Value
+ ///
+ /// Load settings
+ ///
+ /// Setting class type
+ /// Store identifier for which settigns should be loaded
+ ISettings LoadSetting(Type settingType, int storeId = 0);
+
+ ///
+ /// Set setting value
+ ///
+ /// Type
+ /// Key
+ /// Value
/// Store identifier
- /// A value indicating whether to clear cache after setting update
- void SetSetting(string key, T value, int storeId = 0, bool clearCache = true);
+ /// A value indicating whether to clear cache after setting update
+ void SetSetting(string key, T value, int storeId = 0, bool clearCache = true);
///
/// Save settings object
@@ -89,6 +96,13 @@ bool SettingExists(T settings,
/// Store identifier
void SaveSetting(T settings, int storeId = 0) where T : ISettings, new();
+ ///
+ /// Save settings object
+ ///
+ /// Setting instance
+ /// Store identifier
+ void SaveSetting(ISettings settings, int storeId = 0);
+
///
/// Save settings object
///
diff --git a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
index d0ed7563d2..00d96fe9ba 100644
--- a/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
+++ b/src/Libraries/SmartStore.Core/Configuration/JsonPersistAttribute.cs
@@ -4,17 +4,15 @@
using System.Text;
namespace SmartStore.Core.Configuration
-{
-
- // codehint: sm-add
+{
///
/// Marker attribute. Indicates that the settings should
/// be persisted as a JSON string rather than splitted
/// into single properties.
///
[AttributeUsage(AttributeTargets.Class)]
+ [Obsolete]
public class JsonPersistAttribute : Attribute
{
}
-
}
diff --git a/src/Libraries/SmartStore.Core/Data/DataSettings.cs b/src/Libraries/SmartStore.Core/Data/DataSettings.cs
index f903cadc9a..146cc96bcb 100644
--- a/src/Libraries/SmartStore.Core/Data/DataSettings.cs
+++ b/src/Libraries/SmartStore.Core/Data/DataSettings.cs
@@ -206,7 +206,7 @@ public virtual bool Load()
this.TenantName = curTenant;
this.TenantPath = tenantPath;
- if (File.Exists(filePath))
+ if (File.Exists(filePath) && !s_TestMode)
{
string text = File.ReadAllText(filePath);
var settings = ParseSettings(text);
diff --git a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
index 93a7552c50..7d3b433e8e 100644
--- a/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
+++ b/src/Libraries/SmartStore.Core/Data/DbContextScope.cs
@@ -3,6 +3,7 @@
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
namespace SmartStore.Core.Data
{
@@ -89,7 +90,12 @@ public int Commit()
return _ctx.SaveChanges();
}
- public void Dispose()
+ public async Task CommitAsync()
+ {
+ return await _ctx.SaveChangesAsync();
+ }
+
+ public void Dispose()
{
_ctx.AutoDetectChangesEnabled = _autoDetectChangesEnabled;
_ctx.ProxyCreationEnabled = _proxyCreationEnabled;
diff --git a/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs b/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs
index d1e22d6965..3e2318e4f0 100644
--- a/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs
+++ b/src/Libraries/SmartStore.Core/Data/DbQuerySettings.cs
@@ -4,8 +4,6 @@ namespace SmartStore.Core.Data
{
public class DbQuerySettings
{
- private readonly static DbQuerySettings s_default = new DbQuerySettings(false, false);
-
public DbQuerySettings(bool ignoreAcl, bool ignoreMultiStore)
{
this.IgnoreAcl = ignoreAcl;
@@ -15,9 +13,6 @@ public DbQuerySettings(bool ignoreAcl, bool ignoreMultiStore)
public bool IgnoreAcl { get; private set; }
public bool IgnoreMultiStore { get; private set; }
- public static DbQuerySettings Default
- {
- get { return s_default; }
- }
+ public static DbQuerySettings Default { get; } = new DbQuerySettings(false, false);
}
}
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs
deleted file mode 100644
index 6cebe26609..0000000000
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DbLoadHook.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System;
-
-namespace SmartStore.Core.Data.Hooks
-{
- public abstract class DbLoadHook : IDbLoadHook
- {
- public virtual void OnLoaded(BaseEntity entity)
- {
- }
- }
-}
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs
index b77114ed60..09b74716ac 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/DbSaveHook.cs
@@ -2,10 +2,9 @@
namespace SmartStore.Core.Data.Hooks
{
- public abstract class DbSaveHook : IDbSaveHook
- where TEntity : class
+ public abstract class DbSaveHook : IDbSaveHook where TEntity : class
{
- public void OnBeforeSave(HookedEntity entry)
+ public virtual void OnBeforeSave(IHookedEntity entry)
{
var entity = entry.Entity as TEntity;
switch (entry.InitialState)
@@ -22,24 +21,26 @@ public void OnBeforeSave(HookedEntity entry)
}
}
- protected virtual void OnInserting(TEntity entity, HookedEntity entry)
+ protected virtual void OnInserting(TEntity entity, IHookedEntity entry)
{
+ throw new NotImplementedException();
}
- protected virtual void OnUpdating(TEntity entity, HookedEntity entry)
+ protected virtual void OnUpdating(TEntity entity, IHookedEntity entry)
{
+ throw new NotImplementedException();
}
- protected virtual void OnDeleting(TEntity entity, HookedEntity entry)
+ protected virtual void OnDeleting(TEntity entity, IHookedEntity entry)
{
+ throw new NotImplementedException();
}
public virtual void OnBeforeSaveCompleted()
{
}
-
- public void OnAfterSave(HookedEntity entry)
+ public virtual void OnAfterSave(IHookedEntity entry)
{
var entity = entry.Entity as TEntity;
switch (entry.InitialState)
@@ -56,16 +57,19 @@ public void OnAfterSave(HookedEntity entry)
}
}
- protected virtual void OnInserted(TEntity entity, HookedEntity entry)
+ protected virtual void OnInserted(TEntity entity, IHookedEntity entry)
{
+ throw new NotImplementedException();
}
- protected virtual void OnUpdated(TEntity entity, HookedEntity entry)
+ protected virtual void OnUpdated(TEntity entity, IHookedEntity entry)
{
+ throw new NotImplementedException();
}
- protected virtual void OnDeleted(TEntity entity, HookedEntity entry)
+ protected virtual void OnDeleted(TEntity entity, IHookedEntity entry)
{
+ throw new NotImplementedException();
}
public virtual void OnAfterSaveCompleted()
diff --git a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
index 770f115e1e..39126f48f8 100644
--- a/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
+++ b/src/Libraries/SmartStore.Core/Data/Hooks/DefaultDbHookHandler.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections.Concurrent;
using System.Collections.Generic;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
using SmartStore.Collections;
using SmartStore.Core.Logging;
@@ -10,25 +8,24 @@ namespace SmartStore.Core.Data.Hooks
{
public class DefaultDbHookHandler : IDbHookHandler
{
- private readonly IEnumerable> _hooks;
- private readonly IList> _loadHooks;
private readonly IList> _saveHooks;
- private readonly Multimap _loadHooksRequestCache = new Multimap();
- private readonly Multimap _saveHooksRequestCache = new Multimap();
+ private readonly Multimap _hooksRequestCache = new Multimap();
// Prevents repetitive hooking of the same entity/state/[pre|post] combination within a single request
private readonly HashSet _hookedEntities = new HashSet();
- private readonly static ConcurrentDictionary _hookableEntities = new ConcurrentDictionary();
- private static HashSet _importantLoadHookTypes;
private static HashSet _importantSaveHookTypes;
private readonly static object _lock = new object();
+ // Contains all IDbHook/EntityType/State/Stage combinations in which
+ // the implementor threw either NotImplementedException or NotSupportedException.
+ // This boosts performance because these VOID combinations are not processed again
+ // and frees us mostly from the obligation always to detect changes.
+ private readonly static HashSet _voidHooks = new HashSet();
+
public DefaultDbHookHandler(IEnumerable> hooks)
{
- _hooks = hooks;
- _loadHooks = hooks.Where(x => x.Metadata.IsLoadHook == true).ToList();
_saveHooks = hooks.Where(x => x.Metadata.IsLoadHook == false).ToList();
}
@@ -36,24 +33,7 @@ public ILogger Logger
{
get;
set;
- }
-
- public bool HasImportantLoadHooks()
- {
- if (_importantLoadHookTypes == null)
- {
- lock (_lock)
- {
- if (_importantLoadHookTypes == null)
- {
- _importantLoadHookTypes = new HashSet();
- _importantLoadHookTypes.AddRange(_loadHooks.Where(x => x.Metadata.Important).Select(x => x.Metadata.ImplType));
- }
- }
- }
-
- return _importantLoadHookTypes.Any();
- }
+ } = NullLogger.Instance;
public bool HasImportantSaveHooks()
{
@@ -72,58 +52,29 @@ public bool HasImportantSaveHooks()
return _importantSaveHookTypes.Any();
}
- public void TriggerLoadHooks(BaseEntity entity, bool importantHooksOnly)
- {
- if (!_loadHooks.Any() || (importantHooksOnly && !this.HasImportantLoadHooks()))
- {
- return;
- }
-
- if (entity == null || !IsHookableEntity(entity))
- {
- return;
- }
-
- var loadHooks = GetLoadHookInstancesFor(entity, importantHooksOnly);
- foreach (var hook in loadHooks)
- {
- // call hook
- try
- {
- hook.OnLoaded(entity);
- }
- catch (Exception ex)
- {
- Logger.ErrorFormat(ex, "LoadHook exception ({0})", hook.GetType().FullName);
- }
- }
- }
-
- public bool TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly)
+ public IEnumerable TriggerPreSaveHooks(IEnumerable entries, bool importantHooksOnly, out bool anyStateChanged)
{
- bool anyStateChanged = false;
-
- if (entries != null)
- {
- // Skip entities explicitly marked as unhookable
- entries = entries.Where(IsHookableEntry);
- }
+ Guard.NotNull(entries, nameof(entries));
- if (entries == null || !entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks()))
- return false;
+ anyStateChanged = false;
var processedHooks = new HashSet();
+ if (!entries.Any() || !_saveHooks.Any() || (importantHooksOnly && !this.HasImportantSaveHooks()))
+ return processedHooks;
+
foreach (var entry in entries)
{
var e = entry; // Prevents access to modified closure
- var entity = e.Entity;
- if (HandledAlready(entity, e.InitialState, false))
+
+ if (HandledAlready(e, HookStage.PreSave))
{
// Prevent repetitive hooking of the same entity/state/pre combination within a single request
continue;
}
- var hooks = GetSaveHookInstancesFor(entity, importantHooksOnly);
+
+ var hooks = GetSaveHookInstancesFor(e, HookStage.PreSave, importantHooksOnly);
+
foreach (var hook in hooks)
{
// call hook
@@ -133,6 +84,10 @@ public bool TriggerPreSaveHooks(IEnumerable entries, bool importan
hook.OnBeforeSave(e);
processedHooks.Add(hook);
}
+ catch (Exception ex) when (ex is NotImplementedException || ex is NotSupportedException)
+ {
+ RegisterVoidHook(hook, e.EntityType, e.InitialState, HookStage.PreSave);
+ }
catch (Exception ex)
{
Logger.ErrorFormat(ex, "PreSaveHook exception ({0})", hook.GetType().FullName);
@@ -149,33 +104,31 @@ public bool TriggerPreSaveHooks(IEnumerable entries, bool importan
processedHooks.Each(x => x.OnBeforeSaveCompleted());
- return anyStateChanged;
+ return processedHooks;
}
- public void TriggerPostSaveHooks(IEnumerable