diff --git a/.gitignore b/.gitignore index 5b5e7660c5f6..16994e0ab195 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,4 @@ build/docs.zip build/ui-docs.zip build/csharp-docs.zip build/msbuild.log +.vs/ diff --git a/README.md b/README.md index 3184d2024d99..3e9be6cb5f55 100644 --- a/README.md +++ b/README.md @@ -1,42 +1,52 @@ Umbraco CMS =========== -Umbraco is a free open source Content Management System built on the ASP.NET platform. +The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 350,000 websites worldwide: [https://umbraco.com](https://umbraco.com) + +[![ScreenShot](vimeo.png)](https://vimeo.com/172382998/) + +## Umbraco CMS ## +Umbraco is a free open source Content Management System built on the ASP.NET platform. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social. + ## Building Umbraco from source ## + The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `grunt vs` in `src\Umbraco.Web.UI.Client`. If you're interested in making changes to Belle without running Visual Studio make sure to read the [Belle ReadMe file](src/Umbraco.Web.UI.Client/README.md). Note that you can always [download a nightly build](http://nightly.umbraco.org/?container=umbraco-750) so you don't have to build the code yourself. -## Watch a introduction video ## +## Watch an introduction video ## [![ScreenShot](http://umbraco.com/images/whatisumbraco.png)](https://umbraco.tv/videos/umbraco-v7/content-editor/basics/introduction/cms-explanation/) -## Umbraco - the simple, flexible and friendly ASP.NET CMS ## +## Umbraco - The Friendly CMS ## + +For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. -**More than 350,000 sites trust Umbraco** +Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live. -For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box. It's a developer's dream and your users will love it too. +Used by more than 350,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 200,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay. -Used by more than 350,000 active websites including [http://daviscup.com](http://daviscup.com), [http://heinz.com](http://heinz.com), [http://peugeot.com](http://peugeot.com), [http://www.hersheys.com/](http://www.hersheys.com/) and **The Official ASP.NET and IIS.NET website from Microsoft** ([http://asp.net](http://asp.net) / [http://iis.net](http://iis.net)), you can be sure that the technology is proven, stable and scales. +To view more examples, please visit [https://umbraco.com/why-umbraco/#caseStudies](https://umbraco.com/why-umbraco/#caseStudies) -To view more examples, please visit [http://umbraco.com/why-umbraco/#caseStudies](http://umbraco.com/why-umbraco/#caseStudies) +## Why Open Source? ## +As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use. ## Downloading ## -The downloadable Umbraco releases live at [http://our.umbraco.org/download](http://our.umbraco.org/download). +The downloadable Umbraco releases live at [https://our.umbraco.org/download](https://our.umbraco.org/download). ## Forums ## -We have a forum running on [http://our.umbraco.org](http://our.umbraco.org). The discussions group on [Google Groups](https://groups.google.com/forum/#!forum/umbraco-dev) is for discussions on developing the core, and not on Umbraco-implementations or extensions in general. For those topics, please use [http://our.umbraco.org](http://our.umbraco.org). +Peer-to-peer support is available 24/7 at the community forum on [https://our.umbraco.org](https://our.umbraco.org). ## Contribute to Umbraco ## -If you want to contribute back to Umbraco you should check out our [guide to contributing](http://our.umbraco.org/contribute). +Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](https://our.umbraco.org/contribute). ## Found a bug? ## -Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](http://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). +Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](https://our.umbraco.org/contribute/report-an-issue-or-request-a-feature). To view existing issues, please visit [http://issues.umbraco.org](http://issues.umbraco.org). diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index dbc7be00e4a1..7c1ff15cc8de 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -27,14 +27,14 @@ - + - + - - - + + + diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec index 54d12f684433..d30a139adc2b 100644 --- a/build/NuSpecs/UmbracoCms.nuspec +++ b/build/NuSpecs/UmbracoCms.nuspec @@ -17,7 +17,7 @@ - + diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt index 987db6c3d610..cbb60bb94916 100644 --- a/build/NuSpecs/tools/Web.config.install.xdt +++ b/build/NuSpecs/tools/Web.config.install.xdt @@ -319,6 +319,7 @@ + @@ -334,10 +335,14 @@ - + + + + + diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index df444cdd7bdd..df03fe838139 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,2 +1,2 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) -7.5.3 \ No newline at end of file +7.5.5 \ No newline at end of file diff --git a/src/SQLCE4Umbraco/SqlCEInstaller.cs b/src/SQLCE4Umbraco/SqlCEInstaller.cs index aeb40ed8117e..80a669e59cf1 100644 --- a/src/SQLCE4Umbraco/SqlCEInstaller.cs +++ b/src/SQLCE4Umbraco/SqlCEInstaller.cs @@ -21,15 +21,15 @@ namespace SqlCE4Umbraco public class SqlCEInstaller : DefaultInstallerUtility { #region Private Constants - + /// The latest database version this installer supports. private const DatabaseVersion LatestVersionSupported = DatabaseVersion.Version4_8; /// The specifications to determine the database version. private static readonly VersionSpecs[] m_VersionSpecs = new VersionSpecs[] { - new VersionSpecs("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS LEFT OUTER JOIN umbracoApp ON appAlias = appAlias WHERE CONSTRAINT_NAME = 'FK_umbracoUser2app_umbracoApp'", 0, DatabaseVersion.Version4_8), - new VersionSpecs("SELECT id FROM umbracoNode WHERE id = -21", 1, DatabaseVersion.Version4_1), - new VersionSpecs("SELECT action FROM umbracoAppTree",DatabaseVersion.Version4), + new VersionSpecs("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS LEFT OUTER JOIN umbracoApp ON appAlias = appAlias WHERE CONSTRAINT_NAME = 'FK_umbracoUser2app_umbracoApp'", 0, DatabaseVersion.Version4_8), + new VersionSpecs("SELECT id FROM umbracoNode WHERE id = -21", 1, DatabaseVersion.Version4_1), + new VersionSpecs("SELECT action FROM umbracoAppTree",DatabaseVersion.Version4), new VersionSpecs("SELECT description FROM cmsContentType",DatabaseVersion.Version3), new VersionSpecs("SELECT id FROM sysobjects",DatabaseVersion.None) }; @@ -43,8 +43,9 @@ public class SqlCEInstaller : DefaultInstallerUtility public override bool CanConnect { get - { - SqlHelper.CreateEmptyDatabase(); + { + using (var sqlHelper = SqlHelper) + sqlHelper.CreateEmptyDatabase(); return base.CanConnect; } } @@ -93,22 +94,22 @@ public SqlCEInstaller(SqlCEHelper sqlHelper) : base(sqlHelper, LatestVersionSupp #region DefaultInstaller Members - /// - /// Returns the sql to do a full install - /// - protected override string FullInstallSql - { - get { return string.Empty; } - } + /// + /// Returns the sql to do a full install + /// + protected override string FullInstallSql + { + get { return string.Empty; } + } - /// - /// Returns the sql to do an upgrade - /// - protected override string UpgradeSql - { - get { return string.Empty; } - } + /// + /// Returns the sql to do an upgrade + /// + protected override string UpgradeSql + { + get { return string.Empty; } + } // We need to override this as the default way of detection a db connection checks for systables that doesn't exist // in a CE db @@ -123,8 +124,9 @@ protected override DatabaseVersion DetermineCurrentVersion() // verify connection try { - if (SqlCeApplicationBlock.VerifyConnection(base.SqlHelper.ConnectionString)) - return DatabaseVersion.None; + using (var sqlHelper = SqlHelper) + if (SqlCeApplicationBlock.VerifyConnection(sqlHelper.ConnectionString)) + return DatabaseVersion.None; } catch (Exception e) { diff --git a/src/SQLCE4Umbraco/SqlCETableUtility.cs b/src/SQLCE4Umbraco/SqlCETableUtility.cs index 7c32252d20e1..cac6e0d7848f 100644 --- a/src/SQLCE4Umbraco/SqlCETableUtility.cs +++ b/src/SQLCE4Umbraco/SqlCETableUtility.cs @@ -37,13 +37,15 @@ public override ITable GetTable(string name) ITable table = null; // get name in correct casing - name = SqlHelper.ExecuteScalar("SELECT name FROM sys.tables WHERE name=@name", - SqlHelper.CreateParameter("name", name)); + using (var sqlHelper = SqlHelper) + name = sqlHelper.ExecuteScalar("SELECT name FROM sys.tables WHERE name=@name", + sqlHelper.CreateParameter("name", name)); if (name != null) { table = new DefaultTable(name); - using (IRecordsReader reader = SqlHelper.ExecuteReader( + using (var sqlHelper = SqlHelper) + using (IRecordsReader reader = sqlHelper.ExecuteReader( @"SELECT c.name AS Name, st.name AS DataType, c.max_length, c.is_nullable, c.is_identity FROM sys.tables AS t JOIN sys.columns AS c ON t.object_id = c.object_id @@ -51,7 +53,7 @@ FROM sys.tables AS t JOIN sys.types AS ty ON ty.user_type_id = c.user_type_id JOIN sys.types st ON ty.system_type_id = st.user_type_id WHERE t.name = @name - ORDER BY c.column_id", SqlHelper.CreateParameter("name", name))) + ORDER BY c.column_id", sqlHelper.CreateParameter("name", name))) { while (reader.Read()) { @@ -112,7 +114,8 @@ protected virtual void CreateTable(ITable table) // create query StringBuilder createTableQuery = new StringBuilder(); - createTableQuery.AppendFormat("CREATE TABLE [{0}] (", SqlHelper.EscapeString(table.Name)); + using (var sqlHelper = SqlHelper) + createTableQuery.AppendFormat("CREATE TABLE [{0}] (", sqlHelper.EscapeString(table.Name)); // add fields while (hasNext) @@ -136,7 +139,8 @@ protected virtual void CreateTable(ITable table) // execute query try { - SqlHelper.ExecuteNonQuery(createTableQuery.ToString()); + using (var sqlHelper = SqlHelper) + sqlHelper.ExecuteNonQuery(createTableQuery.ToString()); } catch (Exception executeException) { @@ -154,13 +158,15 @@ protected virtual void CreateColumn(ITable table, IField field) Debug.Assert(table != null && field != null); StringBuilder addColumnQuery = new StringBuilder(); - addColumnQuery.AppendFormat("ALTER TABLE [{0}] ADD [{1}] {2}", - SqlHelper.EscapeString(table.Name), - SqlHelper.EscapeString(field.Name), - SqlHelper.EscapeString(GetDatabaseType(field))); + using (var sqlHelper = SqlHelper) + addColumnQuery.AppendFormat("ALTER TABLE [{0}] ADD [{1}] {2}", + sqlHelper.EscapeString(table.Name), + sqlHelper.EscapeString(field.Name), + sqlHelper.EscapeString(GetDatabaseType(field))); try { - SqlHelper.ExecuteNonQuery(addColumnQuery.ToString()); + using (var sqlHelper = SqlHelper) + sqlHelper.ExecuteNonQuery(addColumnQuery.ToString()); } catch (Exception executeException) { diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index a0a32912c98e..b3b56338645f 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -11,5 +11,5 @@ [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyFileVersion("7.5.3")] -[assembly: AssemblyInformationalVersion("7.5.3")] \ No newline at end of file +[assembly: AssemblyFileVersion("7.5.5")] +[assembly: AssemblyInformationalVersion("7.5.5")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 72e718796009..ccf4aecf138f 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -6,7 +6,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("7.5.3"); + private static readonly Version Version = new Version("7.5.5"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index 7e2bb889649a..d7f45761379c 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -122,6 +122,11 @@ public static class MediaTypes /// MediaType alias for an image. /// public const string Image = "Image"; + + /// + /// MediaType alias indicating allowing auto-selection. + /// + public const string AutoSelect = "umbracoAutoSelect"; } /// diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 7ec45db7be1e..3f9974166c53 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -69,6 +69,11 @@ public static class ObjectTypes /// public const string Document = "C66BA18E-EAF3-4CFF-8A22-41B16D66A972"; + /// + /// Guid for a Document object. + /// + public static readonly Guid DocumentGuid = new Guid(Document); + /// /// Guid for a Document Type object. /// diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index 4da740f4585b..4ef08bd02f1c 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Configuration; using System.IO; using System.Linq; using System.Threading; @@ -30,12 +31,13 @@ using Umbraco.Core.Services; using Umbraco.Core.Sync; using Umbraco.Core.Strings; +using IntegerValidator = Umbraco.Core.PropertyEditors.IntegerValidator; using MigrationsVersionFourNineZero = Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionFourNineZero; namespace Umbraco.Core { /// - /// A bootstrapper for the Umbraco application which initializes all objects for the Core of the application + /// A bootstrapper for the Umbraco application which initializes all objects for the Core of the application /// /// /// This does not provide any startup functionality relating to web objects @@ -191,14 +193,14 @@ protected virtual ApplicationContext CreateApplicationContext(DatabaseContext db protected virtual CacheHelper CreateApplicationCache() { var cacheHelper = new CacheHelper( - //we need to have the dep clone runtime cache provider to ensure + //we need to have the dep clone runtime cache provider to ensure //all entities are cached properly (cloned in and cloned out) new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()), new StaticCacheProvider(), //we have no request based cache when not running in web-based context new NullCacheProvider(), new IsolatedRuntimeCache(type => - //we need to have the dep clone runtime cache provider to ensure + //we need to have the dep clone runtime cache provider to ensure //all entities are cached properly (cloned in and cloned out) new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); @@ -251,18 +253,18 @@ protected virtual void InitializeProfilerResolver() } /// - /// Special method to initialize the ApplicationEventsResolver and any modifications required for it such + /// Special method to initialize the ApplicationEventsResolver and any modifications required for it such /// as adding custom types to the resolver. /// protected virtual void InitializeApplicationEventsResolver() { //find and initialize the application startup handlers, we need to initialize this resolver here because - //it is a special resolver where they need to be instantiated first before any other resolvers in order to bind to + //it is a special resolver where they need to be instantiated first before any other resolvers in order to bind to //events and to call their events during bootup. //ApplicationStartupHandler.RegisterHandlers(); //... and set the special flag to let us resolve before frozen resolution ApplicationEventsResolver.Current = new ApplicationEventsResolver( - ServiceProvider, + ServiceProvider, ProfilingLogger.Logger, PluginManager.ResolveApplicationStartupHandlers()) { @@ -282,7 +284,7 @@ protected virtual void InitializeApplicationRootPath(string rootPath) } /// - /// Fires after initialization and calls the callback to allow for customizations to occur & + /// Fires after initialization and calls the callback to allow for customizations to occur & /// Ensure that the OnApplicationStarting methods of the IApplicationEvents are called /// /// @@ -334,7 +336,7 @@ public virtual IBootManager Complete(Action afterComplete) { if (_isComplete) throw new InvalidOperationException("The boot manager has already been completed"); - + FreezeResolution(); //Here we need to make sure the db can be connected to @@ -366,7 +368,7 @@ public virtual IBootManager Complete(Action afterComplete) ProfilingLogger.Logger.Error("An error occurred running OnApplicationStarted for handler " + x.GetType(), ex); throw; } - }); + }); } //Now, startup all of our legacy startup handler @@ -455,6 +457,10 @@ protected virtual void InitializeResolvers() { ServerRegistrarResolver.Current = new ServerRegistrarResolver(new ConfigServerRegistrar()); } + else if ("true".InvariantEquals(ConfigurationManager.AppSettings["umbracoDisableElectionForSingleServer"])) + { + ServerRegistrarResolver.Current = new ServerRegistrarResolver(new SingleServerRegistrar()); + } else { ServerRegistrarResolver.Current = new ServerRegistrarResolver( @@ -462,7 +468,6 @@ protected virtual void InitializeResolvers() new Lazy(() => ApplicationContext.Services.ServerRegistrationService), new DatabaseServerRegistrarOptions())); } - //by default we'll use the database server messenger with default options (no callbacks), // this will be overridden in the web startup @@ -473,7 +478,7 @@ protected virtual void InitializeResolvers() ServiceProvider, ProfilingLogger.Logger, () => PluginManager.ResolveAssignedMapperTypes()); - + //RepositoryResolver.Current = new RepositoryResolver( // new RepositoryFactory(ApplicationCache)); diff --git a/src/Umbraco.Core/DatabaseContext.cs b/src/Umbraco.Core/DatabaseContext.cs index 05aba6b97d66..585f1d51cfe0 100644 --- a/src/Umbraco.Core/DatabaseContext.cs +++ b/src/Umbraco.Core/DatabaseContext.cs @@ -174,7 +174,7 @@ public virtual DatabaseProviders DatabaseProvider /// public void ConfigureEmbeddedDatabaseConnection() { - const string providerName = "System.Data.SqlServerCe.4.0"; + const string providerName = Constants.DatabaseProviders.SqlCe; var connectionString = GetEmbeddedDatabaseConnectionString(); SaveConnectionString(connectionString, providerName); diff --git a/src/Umbraco.Core/Events/MigrationEventArgs.cs b/src/Umbraco.Core/Events/MigrationEventArgs.cs index 008e50d2eebb..89dfe5629417 100644 --- a/src/Umbraco.Core/Events/MigrationEventArgs.cs +++ b/src/Umbraco.Core/Events/MigrationEventArgs.cs @@ -128,8 +128,14 @@ public Version TargetVersion get { return TargetSemVersion.GetVersion(); } } + /// + /// Gets the origin version of the migration, i.e. the one that is currently installed. + /// public SemVersion ConfiguredSemVersion { get; private set; } + /// + /// Gets the target version of the migration. + /// public SemVersion TargetSemVersion { get; private set; } public string ProductName { get; private set; } diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 64dcfc25a0e4..2bdc6cc9822a 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -41,11 +41,7 @@ public static long GetSize(this IFileSystem fs, string path) { using (var file = fs.OpenFile(path)) { - using (var sr = new StreamReader(file)) - { - var str = sr.ReadToEnd(); - return str.Length; - } + return file.Length; } } diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index d708fcb804f5..c466db29af30 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -86,7 +86,7 @@ public static UmbracoMediaFile Save(HttpPostedFileBase file) private void Initialize() { Filename = _fs.GetFileName(Path); - Extension = _fs.GetExtension(Path) != null + Extension = string.IsNullOrEmpty(_fs.GetExtension(Path)) == false ? _fs.GetExtension(Path).Substring(1).ToLowerInvariant() : ""; Url = _fs.GetUrl(Path); diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index 6f4a53919442..8e7efc517144 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,18 +1,21 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO.MemoryMappedFiles; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web.Hosting; using Umbraco.Core.Logging; -using Umbraco.Core.ObjectResolution; namespace Umbraco.Core { - // represents the main domain - class MainDom : IRegisteredObject + /// + /// Represents the main AppDomain running for a given application. + /// + /// + /// There can be only one "main" AppDomain running for a given application at a time. + /// When an AppDomain starts, it tries to acquire the main domain status. + /// When an AppDomain stops (eg the application is restarting) it should release the main domain status. + /// It is possible to register against the MainDom and be notified when it is released. + /// + internal class MainDom : IRegisteredObject { #region Vars @@ -25,7 +28,7 @@ class MainDom : IRegisteredObject private readonly AsyncLock _asyncLock; private IDisposable _asyncLocker; - // event wait handle used to notify current main domain that it should + // event wait handle used to notify current main domain that it should // release the lock because a new domain wants to be the main domain private readonly EventWaitHandle _signal; @@ -34,16 +37,26 @@ class MainDom : IRegisteredObject private volatile bool _signaled; // we have been signaled // actions to run before releasing the main domain - private readonly SortedList _callbacks = new SortedList(); + private readonly SortedList _callbacks = new SortedList(new WeightComparer()); private const int LockTimeoutMilliseconds = 90000; // (1.5 * 60 * 1000) == 1 min 30 seconds + private class WeightComparer : IComparer + { + public int Compare(int x, int y) + { + var result = x.CompareTo(y); + // return "equal" as "greater than" + return result == 0 ? 1 : result; + } + } + #endregion #region Ctor // initializes a new instance of MainDom - public MainDom(ILogger logger) + internal MainDom(ILogger logger) { _logger = logger; @@ -52,22 +65,47 @@ public MainDom(ILogger logger) if (HostingEnvironment.ApplicationID != null) appId = HostingEnvironment.ApplicationID.ReplaceNonAlphanumericChars(string.Empty); - var lockName = "UMBRACO-" + appId + "-MAINDOM-LCK"; + // combining with the physical path because if running on eg IIS Express, + // two sites could have the same appId even though they are different. + // + // now what could still collide is... two sites, running in two different processes + // and having the same appId, and running on the same app physical path + // + // we *cannot* use the process ID here because when an AppPool restarts it is + // a new process for the same application path + + var appPath = HostingEnvironment.ApplicationPhysicalPath; + var hash = (appId + ":::" + appPath).ToSHA1(); + + var lockName = "UMBRACO-" + hash + "-MAINDOM-LCK"; _asyncLock = new AsyncLock(lockName); - var eventName = "UMBRACO-" + appId + "-MAINDOM-EVT"; + var eventName = "UMBRACO-" + hash + "-MAINDOM-EVT"; _signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName); } #endregion - // register a main domain consumer + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. public bool Register(Action release, int weight = 100) { return Register(null, release, weight); } - // register a main domain consumer + /// + /// Registers a resource that requires the current AppDomain to be the main domain to function. + /// + /// An action to execute when registering. + /// An action to execute before the AppDomain releases the main domain status. + /// An optional weight (lower goes first). + /// A value indicating whether it was possible to register. + /// If registering is successful, then the action + /// is guaranteed to execute before the AppDomain releases the main domain status. public bool Register(Action install, Action release, int weight = 100) { lock (_locko) @@ -97,7 +135,7 @@ private void OnSignal(string source) try { - _logger.Debug("Stopping..."); + _logger.Info("Stopping..."); foreach (var callback in _callbacks.Values) { try @@ -109,7 +147,7 @@ private void OnSignal(string source) _logger.Error("Error while running callback, remaining callbacks will not run.", e); throw; } - + } _logger.Debug("Stopped."); } @@ -118,12 +156,12 @@ private void OnSignal(string source) // in any case... _isMainDom = false; _asyncLocker.Dispose(); - _logger.Debug("Released MainDom."); + _logger.Info("Released MainDom."); } } // acquires the main domain - public bool Acquire() + internal bool Acquire() { lock (_locko) // we don't want the hosting environment to interfere by signaling { @@ -131,11 +169,11 @@ public bool Acquire() // the handler is not installed so that would be the hosting environment if (_signaled) { - _logger.Debug("Cannot acquire MainDom (signaled)."); + _logger.Info("Cannot acquire MainDom (signaled)."); return false; } - _logger.Debug("Acquiring MainDom..."); + _logger.Info("Acquiring MainDom..."); // signal other instances that we want the lock, then wait one the lock, // which may timeout, and this is accepted - see comments below @@ -162,7 +200,7 @@ public bool Acquire() HostingEnvironment.RegisterObject(this); - _logger.Debug("Acquired MainDom."); + _logger.Info("Acquired MainDom."); return true; } } @@ -174,7 +212,7 @@ public bool IsMainDom } // IRegisteredObject - public void Stop(bool immediate) + void IRegisteredObject.Stop(bool immediate) { try { diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index 2982713e5aaf..88476f946da0 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -326,7 +326,7 @@ public IEnumerable NoGroupPropertyTypes } } - /// + /// /// A boolean flag indicating if a property type has been removed from this instance. /// /// diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 4af6f9536a16..1ed15a8fb441 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -25,7 +25,7 @@ public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent no NoAccessNodeId = noAccessNode.Id; _protectedNodeId = protectedNode.Id; - _ruleCollection = new ObservableCollection(ruleCollection); + _ruleCollection = new ObservableCollection(ruleCollection); _ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged; } @@ -81,7 +81,7 @@ private class PropertySelectors internal IEnumerable RemovedRules { get { return _removedRules; } - } + } public IEnumerable Rules { @@ -107,10 +107,7 @@ public void RemoveRule(PublicAccessRule rule) public void ClearRules() { - for (var i = _ruleCollection.Count - 1; i >= 0; i--) - { - RemoveRule(_ruleCollection[i]); - } + _ruleCollection.Clear(); } [DataMember] @@ -126,7 +123,7 @@ public int NoAccessNodeId get { return _noAccessNodeId; } set { SetPropertyValueAndDetectChanges(value, ref _noAccessNodeId, Ps.Value.NoAccessNodeIdSelector); } } - + [DataMember] public int ProtectedNodeId { diff --git a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs index c4cd28f6e090..e9c1685bb3c0 100644 --- a/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/PropertyDataDto.cs @@ -11,7 +11,6 @@ internal class PropertyDataDto { [Column("id")] [PrimaryKeyColumn] - [Index(IndexTypes.NonClustered, Name = "IX_cmsPropertyData")] public int Id { get; set; } [Column("contentNodeId")] diff --git a/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs b/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs index 78c8f483f2c4..eda1b94e3705 100644 --- a/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs +++ b/src/Umbraco.Core/ObjectResolution/ApplicationEventsResolver.cs @@ -7,15 +7,20 @@ namespace Umbraco.Core.ObjectResolution { - /// + /// /// A resolver to return all IApplicationEvents objects /// /// - /// This is disposable because after the app has started it should be disposed to release any memory being occupied by instances. + /// This is disposable because after the app has started it should be disposed to release any memory being occupied by instances. + /// Ordering handlers: handlers are ordered by (ascending) weight. By default, handlers from the Umbraco.* or Concorde.* + /// assemblies have a -100 weight whereas any other handler has a weight of +100. A custom weight can be assigned to a handler + /// by marking the class with the WeightAttribute. For example, the CacheRefresherEventHandler is marked with [Weight(int.MinValue)] + /// because its events need to run before anything else. Positive weights are considered "user-space" while negative weights are + /// "core". Finally, users can register a filter to process the list (after it has been ordered) and re-order it, or remove handlers. + /// BEWARE! handlers order is an important thing, and removing handlers or reordering handlers can have unexpected consequences. /// - internal sealed class ApplicationEventsResolver : ManyObjectsResolverBase, IDisposable + public sealed class ApplicationEventsResolver : ManyObjectsResolverBase, IDisposable { - private readonly LegacyStartupHandlerResolver _legacyResolver; /// @@ -23,18 +28,18 @@ internal sealed class ApplicationEventsResolver : ManyObjectsResolverBase /// /// - /// + /// internal ApplicationEventsResolver(IServiceProvider serviceProvider, ILogger logger, IEnumerable applicationEventHandlers) : base(serviceProvider, logger, applicationEventHandlers) - { + { //create the legacy resolver and only include the legacy types - _legacyResolver = new LegacyStartupHandlerResolver( + _legacyResolver = new LegacyStartupHandlerResolver( serviceProvider, logger, - applicationEventHandlers.Where(x => !TypeHelper.IsTypeAssignableFrom(x))); + applicationEventHandlers.Where(x => TypeHelper.IsTypeAssignableFrom(x) == false)); } /// - /// Override in order to only return types of IApplicationEventHandler and above, + /// Override in order to only return types of IApplicationEventHandler and above, /// do not include the legacy types of IApplicationStartupHandler /// protected override IEnumerable InstanceTypes @@ -42,14 +47,60 @@ protected override IEnumerable InstanceTypes get { return base.InstanceTypes.Where(TypeHelper.IsTypeAssignableFrom); } } - /// - /// Gets the implementations. - /// - public IEnumerable ApplicationEventHandlers + private List _orderedAndFiltered; + + /// + /// Gets the implementations. + /// + public IEnumerable ApplicationEventHandlers { - get { return Values; } + get + { + if (_orderedAndFiltered == null) + { + _orderedAndFiltered = GetSortedValues().ToList(); + if (FilterCollection != null) + FilterCollection(_orderedAndFiltered); + } + return _orderedAndFiltered; + } } + /// + /// Gets or sets a delegate to filter the event handler list (EXPERT!). + /// + /// + /// This can be set on startup in the pre-boot process in either a custom boot manager or global.asax (UmbracoApplication). + /// Allows custom logic to execute in order to filter and/or re-order the event handlers prior to executing. + /// To be used by custom boot sequences where the boot loader needs to remove some handlers, or raise their priority. + /// Filtering the event handler collection can have ugly consequences. Use with care. + /// + public Action> FilterCollection + { + get { return _filterCollection; } + set + { + if (_orderedAndFiltered != null) + throw new InvalidOperationException("Cannot set the FilterCollection delegate once the ApplicationEventHandlers are resolved"); + if (_filterCollection != null) + throw new InvalidOperationException("Cannot set the FilterCollection delegate once it's already been specified"); + + _filterCollection = value; + } + } + + protected override int GetObjectWeight(object o) + { + var type = o.GetType(); + var attr = type.GetCustomAttribute(true); + if (attr != null) return attr.Weight; + var name = type.Assembly.FullName; + + // we should really attribute all our Core handlers, so this is temp + var core = name.InvariantStartsWith("Umbraco.") || name.InvariantStartsWith("Concorde."); + return core ? -DefaultPluginWeight : DefaultPluginWeight; + } + /// /// Create instances of all of the legacy startup handlers /// @@ -62,11 +113,11 @@ public void InstantiateLegacyStartupHandlers() protected override bool SupportsClear { get { return false; } - } + } protected override bool SupportsInsert { - get { return false; } + get { return false; } } private class LegacyStartupHandlerResolver : ManyObjectsResolverBase, IDisposable @@ -90,8 +141,9 @@ public void Dispose() private bool _disposed; private readonly ReaderWriterLockSlim _disposalLocker = new ReaderWriterLockSlim(); + private Action> _filterCollection; - /// + /// /// Gets a value indicating whether this instance is disposed. /// /// @@ -146,7 +198,8 @@ private void DisposeResources() { _legacyResolver.Dispose(); ResetCollections(); + _orderedAndFiltered.Clear(); + _orderedAndFiltered = null; } - - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs b/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs index 2851a70e1580..821b93ba033c 100644 --- a/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs +++ b/src/Umbraco.Core/ObjectResolution/LazyManyObjectsResolverbase.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.ObjectResolution /// for when there is some processing overhead (i.e. Type finding in assemblies) to return the Types used to instantiate the instances. /// In some these cases we don't want to have to type-find during application startup, only when we need to resolve the instances. /// Important notes about this resolver: it does not support Insert or Remove and therefore does not support any ordering unless - /// the types are marked with the WeightedPluginAttribute. + /// the types are marked with the WeightAttribute. /// public abstract class LazyManyObjectsResolverBase : ManyObjectsResolverBase where TResolved : class diff --git a/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs b/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs index 1cfa81228a9d..5e170f47f4ed 100644 --- a/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs +++ b/src/Umbraco.Core/ObjectResolution/ManyObjectsResolverBase.cs @@ -14,18 +14,18 @@ namespace Umbraco.Core.ObjectResolution /// The type of the concrete resolver class. /// The type of the resolved objects. public abstract class ManyObjectsResolverBase : ResolverBase - where TResolved : class + where TResolved : class where TResolver : ResolverBase - { - private Lazy> _applicationInstances; - private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); - private readonly string _httpContextKey; - private readonly List _instanceTypes = new List(); - private IEnumerable _sortedValues; + { + private Lazy> _applicationInstances; + private readonly ReaderWriterLockSlim _lock = new ReaderWriterLockSlim(); + private readonly string _httpContextKey; + private readonly List _instanceTypes = new List(); + private IEnumerable _sortedValues; - private int _defaultPluginWeight = 10; + private int _defaultPluginWeight = 100; - #region Constructors + #region Constructors /// /// Initializes a new instance of the class with an empty list of objects, @@ -61,11 +61,11 @@ protected ManyObjectsResolverBase(IServiceProvider serviceProvider, ILogger logg [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use ctor specifying IServiceProvider instead")] - protected ManyObjectsResolverBase(ObjectLifetimeScope scope = ObjectLifetimeScope.Application) + protected ManyObjectsResolverBase(ObjectLifetimeScope scope = ObjectLifetimeScope.Application) : this(new ActivatorServiceProvider(), LoggerResolver.Current.Logger, scope) - { - - } + { + + } /// /// Initializes a new instance of the class with an empty list of objects, @@ -76,19 +76,19 @@ protected ManyObjectsResolverBase(ObjectLifetimeScope scope = ObjectLifetimeScop /// The HttpContextBase corresponding to the HttpRequest. /// is null. protected ManyObjectsResolverBase(IServiceProvider serviceProvider, ILogger logger, HttpContextBase httpContext) - { + { if (serviceProvider == null) throw new ArgumentNullException("serviceProvider"); if (httpContext == null) throw new ArgumentNullException("httpContext"); CanResolveBeforeFrozen = false; Logger = logger; - LifetimeScope = ObjectLifetimeScope.HttpRequest; - _httpContextKey = GetType().FullName; + LifetimeScope = ObjectLifetimeScope.HttpRequest; + _httpContextKey = GetType().FullName; ServiceProvider = serviceProvider; CurrentHttpContext = httpContext; - _instanceTypes = new List(); + _instanceTypes = new List(); InitializeAppInstances(); - } + } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use ctor specifying IServiceProvider instead")] @@ -110,57 +110,57 @@ protected ManyObjectsResolverBase(HttpContextBase httpContext) /// is per HttpRequest but the current HttpContext is null. protected ManyObjectsResolverBase(IServiceProvider serviceProvider, ILogger logger, IEnumerable value, ObjectLifetimeScope scope = ObjectLifetimeScope.Application) : this(serviceProvider, logger, scope) - { - _instanceTypes = value.ToList(); - } + { + _instanceTypes = value.ToList(); + } [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("Use ctor specifying IServiceProvider instead")] protected ManyObjectsResolverBase(IEnumerable value, ObjectLifetimeScope scope = ObjectLifetimeScope.Application) : this(new ActivatorServiceProvider(), LoggerResolver.Current.Logger, value, scope) { - + } - /// - /// Initializes a new instance of the class with an initial list of objects, - /// with creation of objects based on an HttpRequest lifetime scope. - /// - /// The HttpContextBase corresponding to the HttpRequest. - /// The list of object types. - /// is null. + /// + /// Initializes a new instance of the class with an initial list of objects, + /// with creation of objects based on an HttpRequest lifetime scope. + /// + /// The HttpContextBase corresponding to the HttpRequest. + /// The list of object types. + /// is null. [Obsolete("Use ctor specifying IServiceProvider instead")] protected ManyObjectsResolverBase(HttpContextBase httpContext, IEnumerable value) : this(new ActivatorServiceProvider(), LoggerResolver.Current.Logger, httpContext) - { - _instanceTypes = value.ToList(); - } - #endregion + { + _instanceTypes = value.ToList(); + } + #endregion private void InitializeAppInstances() { _applicationInstances = new Lazy>(() => CreateInstances().ToArray()); } - /// - /// Gets or sets a value indicating whether the resolver can resolve objects before resolution is frozen. - /// - /// This is false by default and is used for some special internal resolvers. - internal bool CanResolveBeforeFrozen { get; set; } + /// + /// Gets or sets a value indicating whether the resolver can resolve objects before resolution is frozen. + /// + /// This is false by default and is used for some special internal resolvers. + internal bool CanResolveBeforeFrozen { get; set; } - /// - /// Gets the list of types to create instances from. - /// - protected virtual IEnumerable InstanceTypes - { - get { return _instanceTypes; } - } + /// + /// Gets the list of types to create instances from. + /// + protected virtual IEnumerable InstanceTypes + { + get { return _instanceTypes; } + } - /// - /// Gets or sets the used to initialize this object, if any. - /// - /// If not null, then LifetimeScope will be ObjectLifetimeScope.HttpRequest. - protected HttpContextBase CurrentHttpContext { get; private set; } + /// + /// Gets or sets the used to initialize this object, if any. + /// + /// If not null, then LifetimeScope will be ObjectLifetimeScope.HttpRequest. + protected HttpContextBase CurrentHttpContext { get; private set; } /// /// Returns the service provider used to instantiate objects @@ -174,16 +174,16 @@ protected virtual IEnumerable InstanceTypes /// protected ObjectLifetimeScope LifetimeScope { get; private set; } - /// - /// Gets the resolved object instances, sorted by weight. - /// - /// The sorted resolved object instances. - /// - /// The order is based upon the WeightedPluginAttribute and DefaultPluginWeight. - /// Weights are sorted ascendingly (lowest weights come first). - /// - protected IEnumerable GetSortedValues() - { + /// + /// Gets the resolved object instances, sorted by weight. + /// + /// The sorted resolved object instances. + /// + /// The order is based upon the WeightAttribute and DefaultPluginWeight. + /// Weights are sorted ascendingly (lowest weights come first). + /// + protected IEnumerable GetSortedValues() + { if (_sortedValues == null) { var values = Values.ToList(); @@ -191,18 +191,18 @@ protected IEnumerable GetSortedValues() _sortedValues = values; } return _sortedValues; - } + } - /// - /// Gets or sets the default type weight. - /// - /// Determines the weight of types that do not have a WeightedPluginAttribute set on - /// them, when calling GetSortedValues. - protected virtual int DefaultPluginWeight - { - get { return _defaultPluginWeight; } - set { _defaultPluginWeight = value; } - } + /// + /// Gets or sets the default type weight. + /// + /// Determines the weight of types that do not have a WeightAttribute set on + /// them, when calling GetSortedValues. + protected virtual int DefaultPluginWeight + { + get { return _defaultPluginWeight; } + set { _defaultPluginWeight = value; } + } /// /// Returns the weight of an object for user with GetSortedValues @@ -210,22 +210,22 @@ protected virtual int DefaultPluginWeight /// /// protected virtual int GetObjectWeight(object o) - { - var type = o.GetType(); - var attr = type.GetCustomAttribute(true); - return attr == null ? DefaultPluginWeight : attr.Weight; - } - - /// - /// Gets the resolved object instances. - /// - /// CanResolveBeforeFrozen is false, and resolution is not frozen. - protected IEnumerable Values - { - get - { - using (Resolution.Reader(CanResolveBeforeFrozen)) - { + { + var type = o.GetType(); + var attr = type.GetCustomAttribute(true); + return attr == null ? DefaultPluginWeight : attr.Weight; + } + + /// + /// Gets the resolved object instances. + /// + /// CanResolveBeforeFrozen is false, and resolution is not frozen. + protected IEnumerable Values + { + get + { + using (Resolution.Reader(CanResolveBeforeFrozen)) + { // note: we apply .ToArray() to the output of CreateInstance() because that is an IEnumerable that // comes from the PluginManager we want to be _sure_ that it's not a Linq of some sort, but the // instances have actually been instanciated when we return. @@ -247,7 +247,7 @@ protected IEnumerable Values CurrentHttpContext.Items[_httpContextKey] = instances; } return (TResolved[])CurrentHttpContext.Items[_httpContextKey]; - + case ObjectLifetimeScope.Application: return _applicationInstances.Value; @@ -258,132 +258,132 @@ protected IEnumerable Values return CreateInstances().ToArray(); } } - } - } + } + } - /// - /// Creates the object instances for the types contained in the types collection. - /// - /// A list of objects of type . - protected virtual IEnumerable CreateInstances() - { - return ServiceProvider.CreateInstances(InstanceTypes, Logger); - } + /// + /// Creates the object instances for the types contained in the types collection. + /// + /// A list of objects of type . + protected virtual IEnumerable CreateInstances() + { + return ServiceProvider.CreateInstances(InstanceTypes, Logger); + } - #region Types collection manipulation + #region Types collection manipulation - /// - /// Removes a type. - /// - /// The type to remove. - /// the resolver does not support removing types, or - /// the type is not a valid type for the resolver. - public virtual void RemoveType(Type value) - { - EnsureSupportsRemove(); - - using (Resolution.Configuration) - using (var l = new UpgradeableReadLock(_lock)) - { - EnsureCorrectType(value); - - l.UpgradeToWriteLock(); - _instanceTypes.Remove(value); - } - } - - /// - /// Removes a type. - /// - /// The type to remove. - /// the resolver does not support removing types, or - /// the type is not a valid type for the resolver. - public void RemoveType() + /// + /// Removes a type. + /// + /// The type to remove. + /// the resolver does not support removing types, or + /// the type is not a valid type for the resolver. + public virtual void RemoveType(Type value) + { + EnsureSupportsRemove(); + + using (Resolution.Configuration) + using (var l = new UpgradeableReadLock(_lock)) + { + EnsureCorrectType(value); + + l.UpgradeToWriteLock(); + _instanceTypes.Remove(value); + } + } + + /// + /// Removes a type. + /// + /// The type to remove. + /// the resolver does not support removing types, or + /// the type is not a valid type for the resolver. + public void RemoveType() where T : TResolved - { - RemoveType(typeof(T)); - } + { + RemoveType(typeof(T)); + } - /// - /// Adds types. - /// - /// The types to add. - /// The types are appended at the end of the list. - /// the resolver does not support adding types, or - /// a type is not a valid type for the resolver, or a type is already in the collection of types. - protected void AddTypes(IEnumerable types) - { - EnsureSupportsAdd(); - - using (Resolution.Configuration) - using (new WriteLock(_lock)) - { - foreach(var t in types) - { - EnsureCorrectType(t); + /// + /// Adds types. + /// + /// The types to add. + /// The types are appended at the end of the list. + /// the resolver does not support adding types, or + /// a type is not a valid type for the resolver, or a type is already in the collection of types. + protected void AddTypes(IEnumerable types) + { + EnsureSupportsAdd(); + + using (Resolution.Configuration) + using (new WriteLock(_lock)) + { + foreach (var t in types) + { + EnsureCorrectType(t); if (_instanceTypes.Contains(t)) - { - throw new InvalidOperationException(string.Format( - "Type {0} is already in the collection of types.", t.FullName)); - } - _instanceTypes.Add(t); - } - } - } - - /// - /// Adds a type. - /// - /// The type to add. - /// The type is appended at the end of the list. - /// the resolver does not support adding types, or - /// the type is not a valid type for the resolver, or the type is already in the collection of types. - public virtual void AddType(Type value) - { - EnsureSupportsAdd(); - - using (Resolution.Configuration) - using (var l = new UpgradeableReadLock(_lock)) - { - EnsureCorrectType(value); + { + throw new InvalidOperationException(string.Format( + "Type {0} is already in the collection of types.", t.FullName)); + } + _instanceTypes.Add(t); + } + } + } + + /// + /// Adds a type. + /// + /// The type to add. + /// The type is appended at the end of the list. + /// the resolver does not support adding types, or + /// the type is not a valid type for the resolver, or the type is already in the collection of types. + public virtual void AddType(Type value) + { + EnsureSupportsAdd(); + + using (Resolution.Configuration) + using (var l = new UpgradeableReadLock(_lock)) + { + EnsureCorrectType(value); if (_instanceTypes.Contains(value)) - { - throw new InvalidOperationException(string.Format( - "Type {0} is already in the collection of types.", value.FullName)); - } - - l.UpgradeToWriteLock(); - _instanceTypes.Add(value); - } - } - - /// - /// Adds a type. - /// - /// The type to add. - /// The type is appended at the end of the list. - /// the resolver does not support adding types, or - /// the type is not a valid type for the resolver, or the type is already in the collection of types. - public void AddType() + { + throw new InvalidOperationException(string.Format( + "Type {0} is already in the collection of types.", value.FullName)); + } + + l.UpgradeToWriteLock(); + _instanceTypes.Add(value); + } + } + + /// + /// Adds a type. + /// + /// The type to add. + /// The type is appended at the end of the list. + /// the resolver does not support adding types, or + /// the type is not a valid type for the resolver, or the type is already in the collection of types. + public void AddType() where T : TResolved - { - AddType(typeof(T)); - } + { + AddType(typeof(T)); + } - /// - /// Clears the list of types - /// - /// the resolver does not support clearing types. - public virtual void Clear() - { - EnsureSupportsClear(); + /// + /// Clears the list of types + /// + /// the resolver does not support clearing types. + public virtual void Clear() + { + EnsureSupportsClear(); - using (Resolution.Configuration) - using (new WriteLock(_lock)) - { - _instanceTypes.Clear(); - } - } + using (Resolution.Configuration) + using (new WriteLock(_lock)) + { + _instanceTypes.Clear(); + } + } /// /// WARNING! Do not use this unless you know what you are doing, clear all types registered and instances @@ -399,32 +399,32 @@ internal void ResetCollections() } } - /// - /// Inserts a type at the specified index. - /// - /// The zero-based index at which the type should be inserted. - /// The type to insert. - /// the resolver does not support inserting types, or - /// the type is not a valid type for the resolver, or the type is already in the collection of types. - /// is out of range. - public virtual void InsertType(int index, Type value) - { - EnsureSupportsInsert(); - - using (Resolution.Configuration) - using (var l = new UpgradeableReadLock(_lock)) - { - EnsureCorrectType(value); + /// + /// Inserts a type at the specified index. + /// + /// The zero-based index at which the type should be inserted. + /// The type to insert. + /// the resolver does not support inserting types, or + /// the type is not a valid type for the resolver, or the type is already in the collection of types. + /// is out of range. + public virtual void InsertType(int index, Type value) + { + EnsureSupportsInsert(); + + using (Resolution.Configuration) + using (var l = new UpgradeableReadLock(_lock)) + { + EnsureCorrectType(value); if (_instanceTypes.Contains(value)) - { - throw new InvalidOperationException(string.Format( - "Type {0} is already in the collection of types.", value.FullName)); - } + { + throw new InvalidOperationException(string.Format( + "Type {0} is already in the collection of types.", value.FullName)); + } - l.UpgradeToWriteLock(); - _instanceTypes.Insert(index, value); - } - } + l.UpgradeToWriteLock(); + _instanceTypes.Insert(index, value); + } + } /// /// Inserts a type at the beginning of the list. @@ -445,9 +445,9 @@ public virtual void InsertType(Type value) /// is out of range. public void InsertType(int index) where T : TResolved - { - InsertType(index, typeof(T)); - } + { + InsertType(index, typeof(T)); + } /// /// Inserts a type at the beginning of the list. @@ -458,68 +458,68 @@ public void InsertType() { InsertType(0, typeof(T)); } - + /// - /// Inserts a type before a specified, already existing type. - /// - /// The existing type before which to insert. - /// The type to insert. - /// the resolver does not support inserting types, or - /// one of the types is not a valid type for the resolver, or the existing type is not in the collection, - /// or the new type is already in the collection of types. - public virtual void InsertTypeBefore(Type existingType, Type value) - { - EnsureSupportsInsert(); - - using (Resolution.Configuration) - using (var l = new UpgradeableReadLock(_lock)) - { - EnsureCorrectType(existingType); - EnsureCorrectType(value); + /// Inserts a type before a specified, already existing type. + /// + /// The existing type before which to insert. + /// The type to insert. + /// the resolver does not support inserting types, or + /// one of the types is not a valid type for the resolver, or the existing type is not in the collection, + /// or the new type is already in the collection of types. + public virtual void InsertTypeBefore(Type existingType, Type value) + { + EnsureSupportsInsert(); + + using (Resolution.Configuration) + using (var l = new UpgradeableReadLock(_lock)) + { + EnsureCorrectType(existingType); + EnsureCorrectType(value); if (_instanceTypes.Contains(existingType) == false) - { - throw new InvalidOperationException(string.Format( - "Type {0} is not in the collection of types.", existingType.FullName)); - } + { + throw new InvalidOperationException(string.Format( + "Type {0} is not in the collection of types.", existingType.FullName)); + } if (_instanceTypes.Contains(value)) - { - throw new InvalidOperationException(string.Format( - "Type {0} is already in the collection of types.", value.FullName)); - } + { + throw new InvalidOperationException(string.Format( + "Type {0} is already in the collection of types.", value.FullName)); + } int index = _instanceTypes.IndexOf(existingType); - l.UpgradeToWriteLock(); - _instanceTypes.Insert(index, value); - } - } + l.UpgradeToWriteLock(); + _instanceTypes.Insert(index, value); + } + } - /// - /// Inserts a type before a specified, already existing type. - /// - /// The existing type before which to insert. - /// The type to insert. - /// the resolver does not support inserting types, or - /// one of the types is not a valid type for the resolver, or the existing type is not in the collection, - /// or the new type is already in the collection of types. - public void InsertTypeBefore() + /// + /// Inserts a type before a specified, already existing type. + /// + /// The existing type before which to insert. + /// The type to insert. + /// the resolver does not support inserting types, or + /// one of the types is not a valid type for the resolver, or the existing type is not in the collection, + /// or the new type is already in the collection of types. + public void InsertTypeBefore() where TExisting : TResolved where T : TResolved - { - InsertTypeBefore(typeof(TExisting), typeof(T)); - } + { + InsertTypeBefore(typeof(TExisting), typeof(T)); + } - /// - /// Returns a value indicating whether the specified type is already in the collection of types. - /// - /// The type to look for. - /// A value indicating whether the type is already in the collection of types. - public virtual bool ContainsType(Type value) - { - using (new ReadLock(_lock)) - { - return _instanceTypes.Contains(value); - } - } + /// + /// Returns a value indicating whether the specified type is already in the collection of types. + /// + /// The type to look for. + /// A value indicating whether the type is already in the collection of types. + public virtual bool ContainsType(Type value) + { + using (new ReadLock(_lock)) + { + return _instanceTypes.Contains(value); + } + } /// /// Gets the types in the collection of types. @@ -536,27 +536,27 @@ public virtual IEnumerable GetTypes() return types; } - /// - /// Returns a value indicating whether the specified type is already in the collection of types. - /// - /// The type to look for. - /// A value indicating whether the type is already in the collection of types. - public bool ContainsType() + /// + /// Returns a value indicating whether the specified type is already in the collection of types. + /// + /// The type to look for. + /// A value indicating whether the type is already in the collection of types. + public bool ContainsType() where T : TResolved - { - return ContainsType(typeof(T)); - } + { + return ContainsType(typeof(T)); + } - #endregion + #endregion - /// - /// Returns a WriteLock to use when modifying collections - /// - /// - protected WriteLock GetWriteLock() - { - return new WriteLock(_lock); - } + /// + /// Returns a WriteLock to use when modifying collections + /// + /// + protected WriteLock GetWriteLock() + { + return new WriteLock(_lock); + } #region Type utilities @@ -581,70 +581,71 @@ protected virtual void EnsureCorrectType(Type value) /// /// The resolver does not support removing types. protected void EnsureSupportsRemove() - { - if (SupportsRemove == false) + { + if (SupportsRemove == false) throw new InvalidOperationException("This resolver does not support removing types"); - } + } /// /// Ensures that the resolver supports clearing types. /// /// The resolver does not support clearing types. - protected void EnsureSupportsClear() { - if (SupportsClear == false) + protected void EnsureSupportsClear() + { + if (SupportsClear == false) throw new InvalidOperationException("This resolver does not support clearing types"); - } + } /// /// Ensures that the resolver supports adding types. /// /// The resolver does not support adding types. protected void EnsureSupportsAdd() - { - if (SupportsAdd == false) + { + if (SupportsAdd == false) throw new InvalidOperationException("This resolver does not support adding new types"); - } + } /// /// Ensures that the resolver supports inserting types. /// /// The resolver does not support inserting types. protected void EnsureSupportsInsert() - { - if (SupportsInsert == false) + { + if (SupportsInsert == false) throw new InvalidOperationException("This resolver does not support inserting new types"); - } + } /// /// Gets a value indicating whether the resolver supports adding types. /// protected virtual bool SupportsAdd - { - get { return true; } - } + { + get { return true; } + } /// /// Gets a value indicating whether the resolver supports inserting types. /// protected virtual bool SupportsInsert - { - get { return true; } - } + { + get { return true; } + } /// /// Gets a value indicating whether the resolver supports clearing types. /// protected virtual bool SupportsClear - { - get { return true; } - } + { + get { return true; } + } /// /// Gets a value indicating whether the resolver supports removing types. /// protected virtual bool SupportsRemove - { - get { return true; } + { + get { return true; } } #endregion diff --git a/src/Umbraco.Core/ObjectResolution/WeightedPluginAttribute.cs b/src/Umbraco.Core/ObjectResolution/WeightAttribute.cs similarity index 52% rename from src/Umbraco.Core/ObjectResolution/WeightedPluginAttribute.cs rename to src/Umbraco.Core/ObjectResolution/WeightAttribute.cs index 323142bd4c81..16d18fb7b780 100644 --- a/src/Umbraco.Core/ObjectResolution/WeightedPluginAttribute.cs +++ b/src/Umbraco.Core/ObjectResolution/WeightAttribute.cs @@ -1,25 +1,25 @@ -using System; - -namespace Umbraco.Core.ObjectResolution -{ - /// - /// Indicates the relative weight of a resolved object type. - /// - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - internal class WeightedPluginAttribute : Attribute - { - /// - /// Initializes a new instance of the class with a weight. - /// - /// The object type weight. - public WeightedPluginAttribute(int weight) - { - Weight = weight; - } - - /// - /// Gets or sets the weight of the object type. - /// - public int Weight { get; private set; } - } +using System; + +namespace Umbraco.Core.ObjectResolution +{ + /// + /// Indicates the relative weight of a resolved object type. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class WeightAttribute : Attribute + { + /// + /// Initializes a new instance of the class with a weight. + /// + /// The object type weight. + public WeightAttribute(int weight) + { + Weight = weight; + } + + /// + /// Gets or sets the weight of the object type. + /// + public int Weight { get; private set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/BulkDataReader.cs b/src/Umbraco.Core/Persistence/BulkDataReader.cs new file mode 100644 index 000000000000..8df4dd536ebd --- /dev/null +++ b/src/Umbraco.Core/Persistence/BulkDataReader.cs @@ -0,0 +1,1511 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.Persistence +{ + /// + /// A base implementation of that is suitable for . + /// + /// + /// + /// Borrowed from Microsoft: + /// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ + /// + /// This implementation is designed to be very memory efficient requiring few memory resources and to support + /// rapid transfer of data to SQL Server. + /// + /// Subclasses should implement , , + /// , , . + /// If they contain disposable resources they should override . + /// + /// SD: Alternatively, we could have used a LinqEntityDataReader which is nicer to use but it uses quite a lot of reflection and + /// I thought this would just be quicker. + /// Simple example of that: https://github.com/gridsum/DataflowEx/blob/master/Gridsum.DataflowEx/Databases/BulkDataReader.cs + /// Full example of that: https://github.com/matthewschrager/Repository/blob/master/Repository.EntityFramework/EntityDataReader.cs + /// So we know where to find that if we ever need it, these would convert any Linq data source to an IDataReader + /// + /// + internal abstract class BulkDataReader : IDataReader + { + + #region Fields + + /// + /// The containing the input row set's schema information + /// requires to function correctly. + /// + private DataTable _schemaTable = new DataTable(); + + /// + /// The mapping from the row set input to the target table's columns. + /// + private List _columnMappings = new List(); + + #endregion + + #region Subclass utility routines + + /// + /// The mapping from the row set input to the target table's columns. + /// + /// + /// If necessary, will be called to initialize the mapping. + /// + public ReadOnlyCollection ColumnMappings + { + get + { + if (this._columnMappings.Count == 0) + { + // Need to add the column definitions and mappings. + AddSchemaTableRows(); + + if (this._columnMappings.Count == 0) + { + throw new InvalidOperationException("AddSchemaTableRows did not add rows."); + } + + Debug.Assert(this._schemaTable.Rows.Count == FieldCount); + } + + return new ReadOnlyCollection(_columnMappings); + } + } + + /// + /// The name of the input row set's schema. + /// + /// + /// This may be different from the target schema but usually they are identical. + /// + protected abstract string SchemaName + { + get; + } + + /// + /// The name of the input row set's table. + /// + /// + /// This may be different from the target table but usually they are identical. + /// + protected abstract string TableName + { + get; + } + + /// + /// Adds the input row set's schema to the object. + /// + /// + /// Call + /// to do this for each row. + /// + /// + protected abstract void AddSchemaTableRows(); + + /// + /// For each , the optional columns that may have values. + /// + /// + /// This is used for checking the parameters of . + /// + /// + private static readonly Dictionary> AllowedOptionalColumnCombinations = new Dictionary> + { + { SqlDbType.BigInt, new List { } }, + { SqlDbType.Binary, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Bit, new List { } }, + { SqlDbType.Char, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Date, new List { } }, + { SqlDbType.DateTime, new List { } }, + { SqlDbType.DateTime2, new List { SchemaTableColumn.NumericPrecision } }, + { SqlDbType.DateTimeOffset, new List { SchemaTableColumn.NumericPrecision } }, + { SqlDbType.Decimal, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, + { SqlDbType.Float, new List { SchemaTableColumn.NumericPrecision, SchemaTableColumn.NumericScale } }, + { SqlDbType.Image, new List { } }, + { SqlDbType.Int, new List { } }, + { SqlDbType.Money, new List { } }, + { SqlDbType.NChar, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.NText, new List { } }, + { SqlDbType.NVarChar, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Real, new List { } }, + { SqlDbType.SmallDateTime, new List { } }, + { SqlDbType.SmallInt, new List { } }, + { SqlDbType.SmallMoney, new List { } }, + { SqlDbType.Structured, new List { } }, + { SqlDbType.Text, new List { } }, + { SqlDbType.Time, new List { SchemaTableColumn.NumericPrecision } }, + { SqlDbType.Timestamp, new List { } }, + { SqlDbType.TinyInt, new List { } }, + { SqlDbType.Udt, new List { BulkDataReader.DataTypeNameSchemaColumn } }, + { SqlDbType.UniqueIdentifier, new List { } }, + { SqlDbType.VarBinary, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.VarChar, new List { SchemaTableColumn.ColumnSize } }, + { SqlDbType.Variant, new List { } }, + { SqlDbType.Xml, new List { BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, BulkDataReader.XmlSchemaCollectionNameSchemaColumn } } + }; + + /// + /// A helper method to support . + /// + /// + /// This methds does extensive argument checks. These errors will cause hard to diagnose exceptions in latter + /// processing so it is important to detect them when they can be easily associated with the code defect. + /// + /// + /// The combination of values for the parameters is not supported. + /// + /// + /// A null value for the parameter is not supported. + /// + /// + /// The name of the column. + /// + /// + /// The size of the column which may be null if not applicable. + /// + /// + /// The precision of the column which may be null if not applicable. + /// + /// + /// The scale of the column which may be null if not applicable. + /// + /// + /// Are the column values unique (i.e. never duplicated)? + /// + /// + /// Is the column part of the primary key? + /// + /// + /// Is the column nullable (i.e. optional)? + /// + /// + /// The corresponding . + /// + /// + /// The schema name of the UDT. + /// + /// + /// The type name of the UDT. + /// + /// + /// For XML columns the schema collection's database name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's schema name. Otherwise, null. + /// + /// + /// For XML columns the schema collection's name. Otherwise, null. + /// + /// + protected void AddSchemaTableRow(string columnName, + int? columnSize, + short? numericPrecision, + short? numericScale, + bool isUnique, + bool isKey, + bool allowDbNull, + SqlDbType providerType, + string udtSchema, + string udtType, + string xmlSchemaCollectionDatabase, + string xmlSchemaCollectionOwningSchema, + string xmlSchemaCollectionName) + { + if (string.IsNullOrEmpty(columnName)) + { + throw new ArgumentException("columnName must be a nonempty string."); + } + else if (columnSize.HasValue && columnSize.Value <= 0) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + else if (numericPrecision.HasValue && numericPrecision.Value <= 0) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + else if (numericScale.HasValue && numericScale.Value < 0) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + List allowedOptionalColumnList; + + if (BulkDataReader.AllowedOptionalColumnCombinations.TryGetValue(providerType, out allowedOptionalColumnList)) + { + if ((columnSize.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.ColumnSize)) || + (numericPrecision.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericPrecision)) || + (numericScale.HasValue && !allowedOptionalColumnList.Contains(SchemaTableColumn.NumericScale)) || + (udtSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || + (udtType != null && !allowedOptionalColumnList.Contains(BulkDataReader.DataTypeNameSchemaColumn)) || + (xmlSchemaCollectionDatabase != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn)) || + (xmlSchemaCollectionOwningSchema != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn)) || + (xmlSchemaCollectionName != null && !allowedOptionalColumnList.Contains(BulkDataReader.XmlSchemaCollectionNameSchemaColumn))) + { + throw new ArgumentException("Columns are set that are incompatible with the value of providerType."); + } + } + else + { + throw new ArgumentException("providerType is unsupported."); + } + + Type dataType; // Corresponding CLR type. + string dataTypeName; // Corresponding SQL Server type. + bool isLong = false; // Is the column a large value column (e.g. nvarchar(max))? + + switch (providerType) + { + case SqlDbType.BigInt: + dataType = typeof(long); + dataTypeName = "bigint"; + break; + + case SqlDbType.Binary: + dataType = typeof(byte[]); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"binary\" type columns."); + } + else if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "binary({0})", + columnSize.Value); + break; + + case SqlDbType.Bit: + dataType = typeof(bool); + dataTypeName = "bit"; + break; + + case SqlDbType.Char: + dataType = typeof(string); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"char\" type columns."); + } + else if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "char({0})", + columnSize.Value); + break; + + case SqlDbType.Date: + dataType = typeof(DateTime); + dataTypeName = "date"; + break; + + case SqlDbType.DateTime: + dataType = typeof(DateTime); + dataTypeName = "datetime"; + break; + + case SqlDbType.DateTime2: + dataType = typeof(DateTime); + + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "datetime2({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetime2"; + } + break; + + case SqlDbType.DateTimeOffset: + dataType = typeof(DateTimeOffset); + + if (numericPrecision.HasValue) + { + if (numericPrecision.Value > 7) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "datetimeoffset({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "datetimeoffset"; + } + break; + + case SqlDbType.Decimal: + dataType = typeof(decimal); + + if (!numericPrecision.HasValue || !numericScale.HasValue) + { + throw new ArgumentException("numericPrecision and numericScale must be specified for \"decimal\" type columns."); + } + else if (numericPrecision > 38) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + else if (numericScale.Value > numericPrecision.Value) + { + throw new ArgumentException("numericScale must not be larger than numericPrecision for \"decimal\" type columns."); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "decimal({0}, {1})", + numericPrecision.Value, + numericScale.Value); + break; + + case SqlDbType.Float: + dataType = typeof(double); + + if (!numericPrecision.HasValue) + { + throw new ArgumentException("numericPrecision must be specified for \"float\" type columns"); + } + else if (numericPrecision > 53) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "float({0})", + numericPrecision.Value); + break; + + case SqlDbType.Image: + dataType = typeof(byte[]); + dataTypeName = "image"; + break; + + case SqlDbType.Int: + dataType = typeof(int); + dataTypeName = "int"; + break; + + case SqlDbType.Money: + dataType = typeof(decimal); + dataTypeName = "money"; + break; + + case SqlDbType.NChar: + dataType = typeof(string); + + if (!columnSize.HasValue) + { + throw new ArgumentException("columnSize must be specified for \"nchar\" type columns"); + } + else if (columnSize > 4000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "nchar({0})", + columnSize.Value); + break; + + case SqlDbType.NText: + dataType = typeof(string); + dataTypeName = "ntext"; + break; + + case SqlDbType.NVarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 4000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "nvarchar({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "nvarchar(max)"; + } + break; + + case SqlDbType.Real: + dataType = typeof(float); + dataTypeName = "real"; + break; + + case SqlDbType.SmallDateTime: + dataType = typeof(DateTime); + dataTypeName = "smalldatetime"; + break; + + case SqlDbType.SmallInt: + dataType = typeof(Int16); + dataTypeName = "smallint"; + break; + + case SqlDbType.SmallMoney: + dataType = typeof(decimal); + dataTypeName = "smallmoney"; + break; + + // SqlDbType.Structured not supported because it related to nested rowsets. + + case SqlDbType.Text: + dataType = typeof(string); + dataTypeName = "text"; + break; + + case SqlDbType.Time: + dataType = typeof(TimeSpan); + + if (numericPrecision.HasValue) + { + if (numericPrecision > 7) + { + throw new ArgumentOutOfRangeException("numericPrecision"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "time({0})", + numericPrecision.Value); + } + else + { + dataTypeName = "time"; + } + break; + + + // SqlDbType.Timestamp not supported because rowversions are not settable. + + case SqlDbType.TinyInt: + dataType = typeof(byte); + dataTypeName = "tinyint"; + break; + + case SqlDbType.Udt: + if (string.IsNullOrEmpty(udtSchema)) + { + throw new ArgumentException("udtSchema must be nonnull and nonempty for \"UDT\" columns."); + } + else if (string.IsNullOrEmpty(udtType)) + { + throw new ArgumentException("udtType must be nonnull and nonempty for \"UDT\" columns."); + } + + dataType = typeof(object); + using (SqlCommandBuilder commandBuilder = new SqlCommandBuilder()) + { + dataTypeName = commandBuilder.QuoteIdentifier(udtSchema) + "." + commandBuilder.QuoteIdentifier(udtType); + } + break; + + case SqlDbType.UniqueIdentifier: + dataType = typeof(Guid); + dataTypeName = "uniqueidentifier"; + break; + + case SqlDbType.VarBinary: + dataType = typeof(byte[]); + + if (columnSize.HasValue) + { + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "varbinary({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "varbinary(max)"; + } + break; + + case SqlDbType.VarChar: + dataType = typeof(string); + + if (columnSize.HasValue) + { + if (columnSize > 8000) + { + throw new ArgumentOutOfRangeException("columnSize"); + } + + dataTypeName = string.Format(CultureInfo.InvariantCulture, + "varchar({0})", + columnSize.Value); + } + else + { + isLong = true; + + dataTypeName = "varchar(max)"; + } + break; + + case SqlDbType.Variant: + dataType = typeof(object); + dataTypeName = "sql_variant"; + break; + + case SqlDbType.Xml: + dataType = typeof(string); + + if (xmlSchemaCollectionName == null) + { + if (xmlSchemaCollectionDatabase != null || xmlSchemaCollectionOwningSchema != null) + { + throw new ArgumentException("xmlSchemaCollectionDatabase and xmlSchemaCollectionOwningSchema must be null if xmlSchemaCollectionName is null for \"xml\" columns."); + } + + dataTypeName = "xml"; + } + else + { + if (xmlSchemaCollectionName.Length == 0) + { + throw new ArgumentException("xmlSchemaCollectionName must be nonempty or null for \"xml\" columns."); + } + else if (xmlSchemaCollectionDatabase != null && + xmlSchemaCollectionDatabase.Length == 0) + { + throw new ArgumentException("xmlSchemaCollectionDatabase must be null or nonempty for \"xml\" columns."); + } + else if (xmlSchemaCollectionOwningSchema != null && + xmlSchemaCollectionOwningSchema.Length == 0) + { + throw new ArgumentException("xmlSchemaCollectionOwningSchema must be null or nonempty for \"xml\" columns."); + } + + System.Text.StringBuilder schemaCollection = new System.Text.StringBuilder("xml("); + + if (xmlSchemaCollectionDatabase != null) + { + schemaCollection.Append("[" + xmlSchemaCollectionDatabase + "]"); + } + + schemaCollection.Append("[" + (xmlSchemaCollectionOwningSchema == null ? SchemaName : xmlSchemaCollectionOwningSchema) + "]"); + schemaCollection.Append("[" + xmlSchemaCollectionName + "]"); + + dataTypeName = schemaCollection.ToString(); + } + break; + + default: + throw new ArgumentOutOfRangeException("providerType"); + + } + + this._schemaTable.Rows.Add(columnName, + _schemaTable.Rows.Count, + columnSize, + numericPrecision, + numericScale, + isUnique, + isKey, + "TraceServer", + "TraceWarehouse", + columnName, + SchemaName, + TableName, + dataType, + allowDbNull, + providerType, + false, // isAliased + false, // isExpression + false, // isIdentity, + false, // isAutoIncrement, + false, // isRowVersion, + false, // isHidden, + isLong, + true, // isReadOnly, + dataType, + dataTypeName, + xmlSchemaCollectionDatabase, + xmlSchemaCollectionOwningSchema, + xmlSchemaCollectionName); + + this._columnMappings.Add(new SqlBulkCopyColumnMapping(columnName, columnName)); + } + + #endregion + + #region Constructors + + private const string IsIdentitySchemaColumn = "IsIdentity"; + + private const string DataTypeNameSchemaColumn = "DataTypeName"; + + private const string XmlSchemaCollectionDatabaseSchemaColumn = "XmlSchemaCollectionDatabase"; + + private const string XmlSchemaCollectionOwningSchemaSchemaColumn = "XmlSchemaCollectionOwningSchema"; + + private const string XmlSchemaCollectionNameSchemaColumn = "XmlSchemaCollectionName"; + + /// + /// Constructor. + /// + protected BulkDataReader() + { + this._schemaTable.Locale = System.Globalization.CultureInfo.InvariantCulture; + + DataColumnCollection columns = _schemaTable.Columns; + + columns.Add(SchemaTableColumn.ColumnName, typeof(System.String)); + columns.Add(SchemaTableColumn.ColumnOrdinal, typeof(System.Int32)); + columns.Add(SchemaTableColumn.ColumnSize, typeof(System.Int32)); + columns.Add(SchemaTableColumn.NumericPrecision, typeof(System.Int16)); + columns.Add(SchemaTableColumn.NumericScale, typeof(System.Int16)); + columns.Add(SchemaTableColumn.IsUnique, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.IsKey, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.BaseServerName, typeof(System.String)); + columns.Add(SchemaTableOptionalColumn.BaseCatalogName, typeof(System.String)); + columns.Add(SchemaTableColumn.BaseColumnName, typeof(System.String)); + columns.Add(SchemaTableColumn.BaseSchemaName, typeof(System.String)); + columns.Add(SchemaTableColumn.BaseTableName, typeof(System.String)); + columns.Add(SchemaTableColumn.DataType, typeof(System.Type)); + columns.Add(SchemaTableColumn.AllowDBNull, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.ProviderType, typeof(System.Int32)); + columns.Add(SchemaTableColumn.IsAliased, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.IsExpression, typeof(System.Boolean)); + columns.Add(BulkDataReader.IsIdentitySchemaColumn, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsAutoIncrement, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsRowVersion, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsHidden, typeof(System.Boolean)); + columns.Add(SchemaTableColumn.IsLong, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.IsReadOnly, typeof(System.Boolean)); + columns.Add(SchemaTableOptionalColumn.ProviderSpecificDataType, typeof(System.Type)); + columns.Add(BulkDataReader.DataTypeNameSchemaColumn, typeof(System.String)); + columns.Add(BulkDataReader.XmlSchemaCollectionDatabaseSchemaColumn, typeof(System.String)); + columns.Add(BulkDataReader.XmlSchemaCollectionOwningSchemaSchemaColumn, typeof(System.String)); + columns.Add(BulkDataReader.XmlSchemaCollectionNameSchemaColumn, typeof(System.String)); + } + + #endregion + + #region IDataReader + + /// + /// Gets a value indicating the depth of nesting for the current row. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns 0. + /// + /// + public int Depth + { + get { return 0; } + } + + /// + /// Gets the number of columns in the current row. (Inherited from .) + /// + /// + public int FieldCount + { + get { return GetSchemaTable().Rows.Count; } + } + + /// + /// Is the bulk copy process open? + /// + bool _isOpen = true; + + /// + /// Gets a value indicating whether the data reader is closed. (Inherited from .) + /// + /// + public bool IsClosed + { + get { return !_isOpen; } + } + + /// + /// Gets the column located at the specified index. (Inherited from .) + /// + /// + /// No column with the specified index was found. + /// + /// + /// The zero-based index of the column to get. + /// + /// + /// The column located at the specified index as an . + /// + /// + public object this[int i] + { + get { return GetValue(i); } + } + + /// + /// Gets the column with the specified name. (Inherited from .) + /// + /// + /// No column with the specified name was found. + /// + /// + /// The name of the column to find. + /// + /// + /// The column located at the specified name as an . + /// + /// + public object this[string name] + { + get { return GetValue(GetOrdinal(name)); } + } + + /// + /// Gets the number of rows changed, inserted, or deleted by execution of the SQL statement. (Inherited from .) + /// + /// + /// Always returns -1 which is the expected behaviour for statements. + /// + /// + public virtual int RecordsAffected + { + get { return -1; } + } + + /// + /// Closes the . (Inherited from .) + /// + /// + public void Close() + { + this._isOpen = false; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public bool GetBoolean(int i) + { + return (bool)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public byte GetByte(int i) + { + return (byte)GetValue(i); + } + + /// + /// Reads a stream of bytes from the specified column offset into the buffer as an array, starting at the given buffer offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of bytes. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of bytes to read. + /// + /// + /// The actual number of bytes read. + /// + /// + public long GetBytes(int i, + long fieldOffset, + byte[] buffer, + int bufferoffset, + int length) + { + byte[] data = (byte[])GetValue(i); + + if (buffer != null) + { + Array.Copy(data, fieldOffset, buffer, bufferoffset, length); + } + + return data.LongLength; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public char GetChar(int i) + { + char result; + + object data = GetValue(i); + char? dataAsChar = data as char?; + char[] dataAsCharArray = data as char[]; + string dataAsString = data as string; + + if (dataAsChar.HasValue) + { + result = dataAsChar.Value; + } + else if (dataAsCharArray != null && + dataAsCharArray.Length == 1) + { + result = dataAsCharArray[0]; + } + else if (dataAsString != null && + dataAsString.Length == 1) + { + result = dataAsString[0]; + } + else + { + throw new InvalidOperationException("GetValue did not return a Char compatible type."); + } + + return result; + } + + /// + /// Reads a stream of characters from the specified column offset into the buffer as an array, starting at the given buffer offset. + /// (Inherited from .) + /// + /// + /// If you pass a buffer that is null, returns the length of the row in bytes. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The index within the field from which to start the read operation. + /// + /// + /// The buffer into which to read the stream of characters. + /// + /// + /// The index for buffer to start the read operation. + /// + /// + /// The number of characters to read. + /// + /// + /// The actual number of characters read. + /// + /// + public long GetChars(int i, + long fieldoffset, + char[] buffer, + int bufferoffset, + int length) + { + object data = GetValue(i); + + string dataAsString = data as string; + char[] dataAsCharArray = data as char[]; + + if (dataAsString != null) + { + dataAsCharArray = dataAsString.ToCharArray((int)fieldoffset, length); + } + else if (dataAsCharArray == null) + { + throw new InvalidOperationException("GetValue did not return either a Char array or a String."); + } + + if (buffer != null) + { + Array.Copy(dataAsCharArray, fieldoffset, buffer, bufferoffset, length); + } + + return dataAsCharArray.LongLength; + } + + /// + /// Returns an IDataReader for the specified column ordinal. (Inherited from .) + /// + /// + /// does not support nested result sets so this method always returns null. + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The for the specified column ordinal (null). + /// + /// + public IDataReader GetData(int i) + { + if (i < 0 || i >= this.FieldCount) + { + throw new ArgumentOutOfRangeException("i"); + } + + return null; + } + + /// + /// The data type information for the specified field. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The data type information for the specified field. + /// + /// + public string GetDataTypeName(int i) + { + return GetFieldType(i).Name; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public DateTime GetDateTime(int i) + { + return (DateTime)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public DateTimeOffset GetDateTimeOffset(int i) + { + return (DateTimeOffset)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public decimal GetDecimal(int i) + { + return (Decimal)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public double GetDouble(int i) + { + return (double)GetValue(i); + } + + /// + /// Gets the information corresponding to the type of that would be returned from . + /// (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The information corresponding to the type of that would be returned from . + /// + /// + public Type GetFieldType(int i) + { + return (Type)GetSchemaTable().Rows[i][SchemaTableColumn.DataType]; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public float GetFloat(int i) + { + return (float)this[i]; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public Guid GetGuid(int i) + { + return (Guid)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public short GetInt16(int i) + { + return (short)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public int GetInt32(int i) + { + return (int)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public long GetInt64(int i) + { + return (long)GetValue(i); + } + + /// + /// Gets the name for the field to find. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The name of the field or the empty string (""), if there is no value to return. + /// + /// + public string GetName(int i) + { + return (string)GetSchemaTable().Rows[i][SchemaTableColumn.ColumnName]; + } + + /// + /// Return the index of the named field. (Inherited from .) + /// + /// + /// The index of the named field was not found. + /// + /// + /// The name of the field to find. + /// + /// + /// The index of the named field. + /// + /// + public int GetOrdinal(string name) + { + if (name == null) // Empty strings are handled as a IndexOutOfRangeException. + { + throw new ArgumentNullException("name"); + } + + int result = -1; + + int rowCount = FieldCount; + + DataRowCollection schemaRows = GetSchemaTable().Rows; + + // Case sensitive search + for (int ordinal = 0; ordinal < rowCount; ordinal++) + { + if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.Ordinal)) + { + result = ordinal; + } + } + + if (result == -1) + { + // Case insensitive search. + for (int ordinal = 0; ordinal < rowCount; ordinal++) + { + if (String.Equals((string)schemaRows[ordinal][SchemaTableColumn.ColumnName], name, StringComparison.OrdinalIgnoreCase)) + { + result = ordinal; + } + } + } + + if (result == -1) + { + throw new IndexOutOfRangeException(name); + } + + return result; + } + + /// + /// Returns a that describes the column metadata of the . (Inherited from .) + /// + /// + /// The is closed. + /// + /// + /// A that describes the column metadata. + /// + /// + public DataTable GetSchemaTable() + { + if (IsClosed) + { + throw new InvalidOperationException("The IDataReader is closed."); + } + + if (_schemaTable.Rows.Count == 0) + { + // Need to add the column definitions and mappings + _schemaTable.TableName = TableName; + + AddSchemaTableRows(); + + Debug.Assert(_schemaTable.Rows.Count == FieldCount); + } + + return _schemaTable; + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public string GetString(int i) + { + return (string)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + public TimeSpan GetTimeSpan(int i) + { + return (TimeSpan)GetValue(i); + } + + /// + /// Gets the value of the specified column as a . (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column. + /// + /// + public abstract object GetValue(int i); + + /// + /// Populates an array of objects with the column values of the current record. (Inherited from .) + /// + /// + /// was null. + /// + /// + /// An array of to copy the attribute fields into. + /// + /// + /// The number of instances of in the array. + /// + /// + public int GetValues(object[] values) + { + if (values == null) + { + throw new ArgumentNullException("values"); + } + + int fieldCount = Math.Min(FieldCount, values.Length); + + for (int i = 0; i < fieldCount; i++) + { + values[i] = GetValue(i); + } + + return fieldCount; + } + + /// + /// Return whether the specified field is set to null. (Inherited from .) + /// + /// + /// The index passed was outside the range of 0 through . + /// + /// + /// The zero-based column ordinal. + /// + /// + /// True if the specified field is set to null; otherwise, false. + /// + /// + public bool IsDBNull(int i) + { + object data = GetValue(i); + + return data == null || Convert.IsDBNull(data); + } + + /// + /// Advances the data reader to the next result, when reading the results of batch SQL statements. (Inherited from .) + /// + /// + /// for returns a single result set so false is always returned. + /// + /// + /// True if there are more rows; otherwise, false. for returns a single result set so false is always returned. + /// + /// + public bool NextResult() + { + return false; + } + + /// + /// Advances the to the next record. (Inherited from .) + /// + /// + /// True if there are more rows; otherwise, false. + /// + /// + public abstract bool Read(); + + #endregion + + #region IDisposable + + /// + /// Has the object been disposed? + /// + bool _disposed = false; + + /// + /// Dispose of any disposable and expensive resources. + /// + /// + /// Is this call the result of a call? + /// + protected virtual void Dispose(bool disposing) + { + if (!this._disposed) + { + this._disposed = true; + + if (disposing) + { + if (_schemaTable != null) + { + _schemaTable.Dispose(); + this._schemaTable = null; + } + + this._columnMappings = null; + + this._isOpen = false; + + GC.SuppressFinalize(this); + } + } + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. (Inherited from .) + /// + /// + public void Dispose() + { + Dispose(true); + } + + /// + /// Finalizer + /// + /// + /// has no unmanaged resources but a subclass may thus a finalizer is required. + /// + ~BulkDataReader() + { + Dispose(false); + } + + #endregion + + } +} diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs index 9b08279716e6..e3c35e01b48b 100644 --- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs +++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions { internal static class DefinitionFactory { - public static TableDefinition GetTableDefinition(Type modelType) + public static TableDefinition GetTableDefinition(ISqlSyntaxProvider syntaxProvider, Type modelType) { //Looks for PetaPoco's TableNameAtribute for the name of the table //If no attribute is set we use the name of the Type as the default convention @@ -32,7 +32,7 @@ public static TableDefinition GetTableDefinition(Type modelType) //Otherwise use the name of the property itself as the default convention var columnAttribute = propertyInfo.FirstAttribute(); string columnName = columnAttribute != null ? columnAttribute.Name : propertyInfo.Name; - var columnDefinition = GetColumnDefinition(modelType, propertyInfo, columnName, tableName); + var columnDefinition = GetColumnDefinition(syntaxProvider, modelType, propertyInfo, columnName, tableName); tableDefinition.Columns.Add(columnDefinition); //Creates a foreignkey definition and adds it to the collection on the table definition @@ -58,7 +58,7 @@ public static TableDefinition GetTableDefinition(Type modelType) return tableDefinition; } - public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo propertyInfo, string columnName, string tableName) + public static ColumnDefinition GetColumnDefinition(ISqlSyntaxProvider syntaxProvider, Type modelType, PropertyInfo propertyInfo, string columnName, string tableName) { var definition = new ColumnDefinition{ Name = columnName, TableName = tableName, ModificationType = ModificationType.Create }; @@ -110,7 +110,7 @@ public static ColumnDefinition GetColumnDefinition(Type modelType, PropertyInfo { //Special case for MySQL as it can't have multiple default DateTime values, which //is what the umbracoServer table definition is trying to create - if (SqlSyntaxContext.SqlSyntaxProvider is MySqlSyntaxProvider && definition.TableName == "umbracoServer" && + if (syntaxProvider is MySqlSyntaxProvider && definition.TableName == "umbracoServer" && definition.TableName.ToLowerInvariant() == "lastNotifiedDate".ToLowerInvariant()) return definition; diff --git a/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs b/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs index bcc1528d3f38..944d5a8a745e 100644 --- a/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs +++ b/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs @@ -99,7 +99,7 @@ public void CreateTable() public void CreateTable(bool overwrite, Type modelType) { - var tableDefinition = DefinitionFactory.GetTableDefinition(modelType); + var tableDefinition = DefinitionFactory.GetTableDefinition(_syntaxProvider, modelType); var tableName = tableDefinition.Name; string createSql = _syntaxProvider.Format(tableDefinition); diff --git a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs index 6fa13654ad0e..3672f80873fa 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentTypeFactory.cs @@ -122,7 +122,7 @@ public ContentTypeDto BuildContentTypeDto(IContentTypeBase entity) else if (entity is IMemberType) nodeObjectType = Constants.ObjectTypes.MemberTypeGuid; else - throw new Exception("oops: invalid entity."); + throw new Exception("Invalid entity."); var contentTypeDto = new ContentTypeDto { diff --git a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs index 5e34fb893677..b4d5d2e56616 100644 --- a/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/MemberTypeReadOnlyFactory.cs @@ -79,7 +79,7 @@ private PropertyGroupCollection GetPropertyTypeGroupCollection(MemberTypeReadOnl { // note: no idea why Id is nullable here, but better check if (groupDto.Id.HasValue == false) - throw new Exception("oops: groupDto.Id has no value."); + throw new Exception("GroupDto.Id has no value."); group.Id = groupDto.Id.Value; } diff --git a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs index ea7c8ab8f95e..40ec415b3004 100644 --- a/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/BaseMapper.cs @@ -8,7 +8,17 @@ namespace Umbraco.Core.Persistence.Mappers { public abstract class BaseMapper { - + private readonly ISqlSyntaxProvider _sqlSyntax; + + protected BaseMapper() : this(SqlSyntaxContext.SqlSyntaxProvider) + { + } + + protected BaseMapper(ISqlSyntaxProvider sqlSyntax) + { + _sqlSyntax = sqlSyntax; + } + internal abstract ConcurrentDictionary PropertyInfoCache { get; } internal abstract void BuildMap(); @@ -58,8 +68,8 @@ internal virtual string GetColumnName(Type dtoType, PropertyInfo dtoProperty) string columnName = columnAttribute.Name; string columnMap = string.Format("{0}.{1}", - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(tableName), - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName(columnName)); + _sqlSyntax.GetQuotedTableName(tableName), + _sqlSyntax.GetQuotedColumnName(columnName)); return columnMap; } } diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs index 1cc29cf95967..5db0d1d3b0fa 100644 --- a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs +++ b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs @@ -3,6 +3,7 @@ using System.Linq.Expressions; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Core.Persistence.Mappers { @@ -16,6 +17,11 @@ public sealed class ContentMapper : BaseMapper { private static readonly ConcurrentDictionary PropertyInfoCacheInstance = new ConcurrentDictionary(); + public ContentMapper(ISqlSyntaxProvider sqlSyntax) : base(sqlSyntax) + { + + } + //NOTE: its an internal class but the ctor must be public since we're using Activator.CreateInstance to create it // otherwise that would fail because there is no public constructor. public ContentMapper() diff --git a/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs b/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs index ea44964219ee..6909c777446b 100644 --- a/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs +++ b/src/Umbraco.Core/Persistence/Mappers/MappingResolver.cs @@ -43,7 +43,7 @@ internal BaseMapper ResolveMapperByType(Type type) { return byAttribute.Result; } - throw new Exception("Invalid Type: A Mapper could not be resolved based on the passed in Type"); + throw new Exception("Invalid Type: A Mapper could not be resolved based on the passed in Type " + type); }); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index 9570024b09f2..8b335994368e 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -149,8 +149,8 @@ private void CreateUmbracNodeData() private void CreateCmsContentTypeData() { _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 532, NodeId = 1031, Alias = Constants.Conventions.MediaTypes.Folder, Icon = "icon-folder", Thumbnail = "icon-folder", IsContainer = false, AllowAtRoot = true }); - _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture" }); - _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document" }); + _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 533, NodeId = 1032, Alias = Constants.Conventions.MediaTypes.Image, Icon = "icon-picture", Thumbnail = "icon-picture", AllowAtRoot = true }); + _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 534, NodeId = 1033, Alias = Constants.Conventions.MediaTypes.File, Icon = "icon-document", Thumbnail = "icon-document", AllowAtRoot = true }); _database.Insert("cmsContentType", "pk", false, new ContentTypeDto { PrimaryKey = 531, NodeId = 1044, Alias = Constants.Conventions.MemberTypes.DefaultAlias, Icon = "icon-user", Thumbnail = "icon-user" }); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index 423c847c47fc..ab477953b49d 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -159,7 +159,7 @@ public DatabaseSchemaResult ValidateSchema() foreach (var item in OrderedTables.OrderBy(x => x.Key)) { - var tableDefinition = DefinitionFactory.GetTableDefinition(item.Value); + var tableDefinition = DefinitionFactory.GetTableDefinition(_sqlSyntaxProvider, item.Value); result.TableDefinitions.Add(tableDefinition); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs new file mode 100644 index 000000000000..c9a0d509e665 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenFiveFive/UpdateAllowedMediaTypesAtRoot.cs @@ -0,0 +1,25 @@ +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenFiveFive +{ + /// + /// See: http://issues.umbraco.org/issue/U4-4196 + /// + [Migration("7.5.5", 1, GlobalSettings.UmbracoMigrationName)] + public class UpdateAllowedMediaTypesAtRoot : MigrationBase + { + public UpdateAllowedMediaTypesAtRoot(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + Execute.Sql("UPDATE cmsContentType SET allowAtRoot = 1 WHERE nodeId = 1032 OR nodeId = 1033"); + } + + public override void Down() + { } + } +} diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs new file mode 100644 index 000000000000..b50c8e5f9405 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/RemovePropertyDataIdIndex.cs @@ -0,0 +1,38 @@ +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + /// + /// See: http://issues.umbraco.org/issue/U4-9188 + /// + [Migration("7.6.0", 0, GlobalSettings.UmbracoMigrationName)] + public class UpdateUniqueIndexOnCmsPropertyData : MigrationBase + { + public UpdateUniqueIndexOnCmsPropertyData(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + //tuple = tablename, indexname, columnname, unique + var indexes = SqlSyntax.GetDefinedIndexes(Context.Database).ToArray(); + var found = indexes.FirstOrDefault( + x => x.Item1.InvariantEquals("cmsPropertyData") + && x.Item2.InvariantEquals("IX_cmsPropertyData")); + + if (found != null) + { + //drop the index + Delete.Index("IX_cmsPropertyData").OnTable("cmsPropertyData"); + } + } + + public override void Down() + { + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PetaPoco.cs b/src/Umbraco.Core/Persistence/PetaPoco.cs index 63278889a206..88ff456ff653 100644 --- a/src/Umbraco.Core/Persistence/PetaPoco.cs +++ b/src/Umbraco.Core/Persistence/PetaPoco.cs @@ -739,6 +739,11 @@ public static bool SplitSqlForPaging(string sql, out string sqlCount, out string /// internal virtual void BuildSqlDbSpecificPagingQuery(DBType databaseType, long skip, long take, string sql, string sqlSelectRemoved, string sqlOrderBy, ref object[] args, out string sqlPage) { + // this is overriden in UmbracoDatabase, and if running SqlServer >=2012, the database type + // is switched from SqlServer to SqlServerCE in order to use the better paging syntax that + // SqlCE supports, and SqlServer >=2012 too. + // so the first case is actually for SqlServer <2012, and second case is CE *and* SqlServer >=2012 + if (databaseType == DBType.SqlServer || databaseType == DBType.Oracle) { sqlSelectRemoved = rxOrderBy.Replace(sqlSelectRemoved, ""); @@ -746,8 +751,16 @@ internal virtual void BuildSqlDbSpecificPagingQuery(DBType databaseType, long sk { sqlSelectRemoved = "peta_inner.* FROM (SELECT " + sqlSelectRemoved + ") peta_inner"; } - sqlPage = string.Format("SELECT * FROM (SELECT ROW_NUMBER() OVER ({0}) peta_rn, {1}) peta_paged WHERE peta_rn>@{2} AND peta_rn<=@{3}", - sqlOrderBy == null ? "ORDER BY (SELECT NULL)" : sqlOrderBy, sqlSelectRemoved, args.Length, args.Length + 1); + + // split to ensure that peta_rn is the last field to be selected, else Page would fail + // the resulting sql is not perfect, NPoco has a much nicer way to do it, but it would require + // importing large parts of NPoco + var pos = sqlSelectRemoved.IndexOf("FROM"); + var sqlColumns = sqlSelectRemoved.Substring(0, pos); + var sqlFrom = sqlSelectRemoved.Substring(pos); + + sqlPage = string.Format("SELECT * FROM (SELECT {0}, ROW_NUMBER() OVER ({1}) peta_rn {2}) peta_paged WHERE peta_rn>@{3} AND peta_rn<=@{4}", + sqlColumns, sqlOrderBy ?? "ORDER BY (SELECT NULL)", sqlFrom, args.Length, args.Length + 1); args = args.Concat(new object[] { skip, skip + take }).ToArray(); } else if (databaseType == DBType.SqlServerCE) @@ -774,7 +787,7 @@ public void BuildPageQueries(long skip, long take, string sql, ref object[] a throw new Exception("Unable to parse SQL statement for paged query"); if (_dbType == DBType.Oracle && sqlSelectRemoved.StartsWith("*")) throw new Exception("Query must alias '*' when performing a paged query.\neg. select t.* from table t order by t.id"); - + BuildSqlDbSpecificPagingQuery(_dbType, skip, take, sql, sqlSelectRemoved, sqlOrderBy, ref args, out sqlPage); } diff --git a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs index a72621d1a5a0..22e66935bff4 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoExtensions.cs @@ -3,9 +3,13 @@ using System.Data; using System.Data.Common; using System.Data.SqlClient; +using System.Data.SqlServerCe; using System.Linq; using System.Text.RegularExpressions; +using MySql.Data.MySqlClient; +using StackExchange.Profiling.Data; using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; @@ -56,7 +60,7 @@ public static class PetaPocoExtensions /// Safely inserts a record, or updates if it exists, based on a unique constraint. /// /// - /// + /// /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object /// passed in will contain the updated value. /// @@ -78,7 +82,7 @@ internal static RecordPersistenceType InsertOrUpdate(this Database db, T poco /// /// /// - /// + /// /// If the entity has a composite key they you need to specify the update command explicitly /// The action that executed, either an insert or an update. If an insert occurred and a PK value got generated, the poco object /// passed in will contain the updated value. @@ -130,7 +134,7 @@ internal static RecordPersistenceType InsertOrUpdate(this Database db, if (rowCount > 0) return RecordPersistenceType.Update; - // failed: does not exist (due to race cond RC2), need to insert + // failed: does not exist (due to race cond RC2), need to insert // loop } } @@ -150,7 +154,7 @@ public static string EscapeAtSymbols(string value) { //this fancy regex will only match a single @ not a double, etc... var regex = new Regex("(?(this Database db, bool overwrite) creator.CreateTable(overwrite); } + /// + /// Performs the bulk insertion in the context of a current transaction with an optional parameter to complete the transaction + /// when finished + /// + /// + /// + /// public static void BulkInsertRecords(this Database db, IEnumerable collection) { //don't do anything if there are no records. @@ -180,7 +191,7 @@ public static void BulkInsertRecords(this Database db, IEnumerable collect using (var tr = db.GetTransaction()) { - db.BulkInsertRecords(collection, tr, true); + db.BulkInsertRecords(collection, tr, SqlSyntaxContext.SqlSyntaxProvider, true, true); // use native, commit } } @@ -192,55 +203,98 @@ public static void BulkInsertRecords(this Database db, IEnumerable collect /// /// /// + /// + /// + /// If this is false this will try to just generate bulk insert statements instead of using the current SQL platform's bulk + /// insert logic. For SQLCE, bulk insert statements do not work so if this is false it will insert one at a time. + /// /// - public static void BulkInsertRecords(this Database db, IEnumerable collection, Transaction tr, bool commitTrans = false) + /// The number of items inserted + public static int BulkInsertRecords(this Database db, + IEnumerable collection, + Transaction tr, + ISqlSyntaxProvider syntaxProvider, + bool useNativeSqlPlatformBulkInsert = true, + bool commitTrans = false) { + //don't do anything if there are no records. if (collection.Any() == false) - return; + { + return 0; + } + + var pd = Database.PocoData.ForType(typeof(T)); + if (pd == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T)); try { - //if it is sql ce or it is a sql server version less than 2008, we need to do individual inserts. - var sqlServerSyntax = SqlSyntaxContext.SqlSyntaxProvider as SqlServerSyntaxProvider; + int processed = 0; - if ((sqlServerSyntax != null && (int)sqlServerSyntax.GetVersionName(db) < (int)SqlServerVersionName.V2008) - || SqlSyntaxContext.SqlSyntaxProvider is SqlCeSyntaxProvider) - { - //SqlCe doesn't support bulk insert statements! + var usedNativeSqlPlatformInserts = useNativeSqlPlatformBulkInsert + && NativeSqlPlatformBulkInsertRecords(db, syntaxProvider, pd, collection, out processed); - foreach (var poco in collection) + if (usedNativeSqlPlatformInserts == false) + { + //if it is sql ce or it is a sql server version less than 2008, we need to do individual inserts. + var sqlServerSyntax = syntaxProvider as SqlServerSyntaxProvider; + if ((sqlServerSyntax != null && (int) sqlServerSyntax.GetVersionName(db) < (int) SqlServerVersionName.V2008) + || syntaxProvider is SqlCeSyntaxProvider) { - db.Insert(poco); + //SqlCe doesn't support bulk insert statements! + foreach (var poco in collection) + { + db.Insert(poco); + } } - } - else - { - string[] sqlStatements; - var cmds = db.GenerateBulkInsertCommand(collection, db.Connection, out sqlStatements); - for (var i = 0; i < sqlStatements.Length; i++) + else { - using (var cmd = cmds[i]) + //we'll need to generate insert statements instead + + string[] sqlStatements; + var cmds = db.GenerateBulkInsertCommand(pd, collection, out sqlStatements); + for (var i = 0; i < sqlStatements.Length; i++) { - cmd.CommandText = sqlStatements[i]; - cmd.ExecuteNonQuery(); + using (var cmd = cmds[i]) + { + cmd.CommandText = sqlStatements[i]; + cmd.ExecuteNonQuery(); + processed++; + } } } } if (commitTrans) { - tr.Complete(); + tr.Complete(); } + return processed; } catch { if (commitTrans) { - tr.Dispose(); + tr.Dispose(); } throw; } + + } + + /// + /// Performs the bulk insertion in the context of a current transaction with an optional parameter to complete the transaction + /// when finished + /// + /// + /// + /// + /// + /// + [Obsolete("Use the method that specifies an SqlSyntaxContext instance instead")] + public static void BulkInsertRecords(this Database db, IEnumerable collection, Transaction tr, bool commitTrans = false) + { + db.BulkInsertRecords(collection, tr, SqlSyntaxContext.SqlSyntaxProvider, commitTrans); } /// @@ -249,45 +303,36 @@ public static void BulkInsertRecords(this Database db, IEnumerable collect /// /// /// - /// /// + /// /// Sql commands with populated command parameters required to execute the sql statement /// - /// The limits for number of parameters are 2100 (in sql server, I think there's many more allowed in mysql). So - /// we need to detect that many params and split somehow. - /// For some reason the 2100 limit is not actually allowed even though the exception from sql server mentions 2100 as a max, perhaps it is 2099 + /// The limits for number of parameters are 2100 (in sql server, I think there's many more allowed in mysql). So + /// we need to detect that many params and split somehow. + /// For some reason the 2100 limit is not actually allowed even though the exception from sql server mentions 2100 as a max, perhaps it is 2099 /// that is max. I've reduced it to 2000 anyways. /// internal static IDbCommand[] GenerateBulkInsertCommand( - this Database db, - IEnumerable collection, - IDbConnection connection, + this Database db, + Database.PocoData pd, + IEnumerable collection, out string[] sql) { - //A filter used below a few times to get all columns except result cols and not the primary key if it is auto-incremental - Func, bool> includeColumn = (data, column) => - { - if (column.Value.ResultColumn) return false; - if (data.TableInfo.AutoIncrement && column.Key == data.TableInfo.PrimaryKey) return false; - return true; - }; - - var pd = Database.PocoData.ForType(typeof(T)); var tableName = db.EscapeTableName(pd.TableInfo.TableName); //get all columns to include and format for sql - var cols = string.Join(", ", + var cols = string.Join(", ", pd.Columns - .Where(c => includeColumn(pd, c)) + .Where(c => IncludeColumn(pd, c)) .Select(c => tableName + "." + db.EscapeSqlIdentifier(c.Key)).ToArray()); var itemArray = collection.ToArray(); //calculate number of parameters per item - var paramsPerItem = pd.Columns.Count(i => includeColumn(pd, i)); - + var paramsPerItem = pd.Columns.Count(i => IncludeColumn(pd, i)); + //Example calc: - // Given: we have 4168 items in the itemArray, each item contains 8 command parameters (values to be inserterted) + // Given: we have 4168 items in the itemArray, each item contains 8 command parameters (values to be inserterted) // 2100 / 8 = 262.5 // Math.Floor(2100 / 8) = 262 items per trans // 4168 / 262 = 15.908... = there will be 16 trans in total @@ -306,14 +351,14 @@ internal static IDbCommand[] GenerateBulkInsertCommand( .Skip(tIndex * (int)itemsPerTrans) .Take((int)itemsPerTrans); - var cmd = db.CreateCommand(connection, ""); + var cmd = db.CreateCommand(db.Connection, string.Empty); var pocoValues = new List(); var index = 0; foreach (var poco in itemsForTrans) { var values = new List(); //get all columns except result cols and not the primary key if it is auto-incremental - foreach (var i in pd.Columns.Where(x => includeColumn(pd, x))) + foreach (var i in pd.Columns.Where(x => IncludeColumn(pd, x))) { db.AddParam(cmd, i.Value.GetValue(poco), "@"); values.Add(string.Format("{0}{1}", "@", index++)); @@ -321,14 +366,211 @@ internal static IDbCommand[] GenerateBulkInsertCommand( pocoValues.Add("(" + string.Join(",", values.ToArray()) + ")"); } - var sqlResult = string.Format("INSERT INTO {0} ({1}) VALUES {2}", tableName, cols, string.Join(", ", pocoValues)); + var sqlResult = string.Format("INSERT INTO {0} ({1}) VALUES {2}", tableName, cols, string.Join(", ", pocoValues)); sqlQueries.Add(sqlResult); commands.Add(cmd); } sql = sqlQueries.ToArray(); - return commands.ToArray(); + return commands.ToArray(); + } + + /// + /// A filter used below a few times to get all columns except result cols and not the primary key if it is auto-incremental + /// + /// + /// + /// + private static bool IncludeColumn(Database.PocoData data, KeyValuePair column) + { + if (column.Value.ResultColumn) return false; + if (data.TableInfo.AutoIncrement && column.Key == data.TableInfo.PrimaryKey) return false; + return true; + } + + /// + /// Bulk insert records with Sql BulkCopy or TableDirect or whatever sql platform specific bulk insert records should be used + /// + /// + /// + /// + /// + /// The number of records inserted + private static bool NativeSqlPlatformBulkInsertRecords(Database db, ISqlSyntaxProvider syntaxProvider, Database.PocoData pd, IEnumerable collection, out int processed) + { + + var dbConnection = db.Connection; + + //unwrap the profiled connection if there is one + var profiledConnection = dbConnection as ProfiledDbConnection; + if (profiledConnection != null) + { + dbConnection = profiledConnection.InnerConnection; + } + + //check if it's SQL or SqlCe + + var sqlConnection = dbConnection as SqlConnection; + if (sqlConnection != null) + { + processed = BulkInsertRecordsSqlServer(db, (SqlServerSyntaxProvider)syntaxProvider, pd, collection); + return true; + } + + var sqlCeConnection = dbConnection as SqlCeConnection; + if (sqlCeConnection != null) + { + processed = BulkInsertRecordsSqlCe(db, pd, collection); + return true; + } + + //could not use the SQL server's specific bulk insert operations + processed = 0; + return false; + + } + + /// + /// Logic used to perform bulk inserts with SqlCe's TableDirect + /// + /// + /// + /// + /// + /// + internal static int BulkInsertRecordsSqlCe(Database db, + Database.PocoData pd, + IEnumerable collection) + { + var cols = pd.Columns.ToArray(); + + using (var cmd = db.CreateCommand(db.Connection, string.Empty)) + { + cmd.CommandText = pd.TableInfo.TableName; + cmd.CommandType = CommandType.TableDirect; + //cmd.Transaction = GetTypedTransaction(db.Connection.); + + //get the real command + using (var sqlCeCommand = GetTypedCommand(cmd)) + { + // This seems to cause problems, I think this is primarily used for retrieval, not + // inserting. see: https://msdn.microsoft.com/en-us/library/system.data.sqlserverce.sqlcecommand.indexname%28v=vs.100%29.aspx?f=255&MSPPError=-2147217396 + //sqlCeCommand.IndexName = pd.TableInfo.PrimaryKey; + + var count = 0; + using (var rs = sqlCeCommand.ExecuteResultSet(ResultSetOptions.Updatable)) + { + var rec = rs.CreateRecord(); + + foreach (var item in collection) + { + for (var i = 0; i < cols.Length; i++) + { + //skip the index if this shouldn't be included (i.e. PK) + if (IncludeColumn(pd, cols[i])) + { + var val = cols[i].Value.GetValue(item); + rec.SetValue(i, val); + } + } + rs.Insert(rec); + count++; + } + } + return count; + } + + } + } + + /// + /// Logic used to perform bulk inserts with SqlServer's BulkCopy + /// + /// + /// + /// + /// + /// + /// + internal static int BulkInsertRecordsSqlServer(Database db, SqlServerSyntaxProvider sqlSyntaxProvider, + Database.PocoData pd, IEnumerable collection) + { + //NOTE: We need to use the original db.Connection here to create the command, but we need to pass in the typed + // connection below to the SqlBulkCopy + using (var cmd = db.CreateCommand(db.Connection, string.Empty)) + { + using (var copy = new SqlBulkCopy( + GetTypedConnection(db.Connection), + SqlBulkCopyOptions.Default, + GetTypedTransaction(cmd.Transaction)) + { + BulkCopyTimeout = 10000, + DestinationTableName = pd.TableInfo.TableName + }) + { + //var cols = pd.Columns.Where(x => IncludeColumn(pd, x)).Select(x => x.Value).ToArray(); + + using (var bulkReader = new PocoDataDataReader(collection, pd, sqlSyntaxProvider)) + { + copy.WriteToServer(bulkReader); + + return bulkReader.RecordsAffected; + } + } + } + } + + + /// + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + private static TConnection GetTypedConnection(IDbConnection connection) + where TConnection : class, IDbConnection + { + var profiled = connection as ProfiledDbConnection; + if (profiled != null) + { + return profiled.InnerConnection as TConnection; + } + return connection as TConnection; + } + + /// + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + private static TTransaction GetTypedTransaction(IDbTransaction connection) + where TTransaction : class, IDbTransaction + { + var profiled = connection as ProfiledDbTransaction; + if (profiled != null) + { + return profiled.WrappedTransaction as TTransaction; + } + return connection as TTransaction; + } + + /// + /// Returns the underlying connection as a typed connection - this is used to unwrap the profiled mini profiler stuff + /// + /// + /// + /// + private static TCommand GetTypedCommand(IDbCommand command) + where TCommand : class, IDbCommand + { + var profiled = command as ProfiledDbCommand; + if (profiled != null) + { + return profiled.InternalCommand as TCommand; + } + return command as TCommand; } [Obsolete("Use the DatabaseSchemaHelper instead")] @@ -405,8 +647,8 @@ public static DatabaseProviders GetDatabaseProvider(this Database db) return ApplicationContext.Current.DatabaseContext.DatabaseProvider; } - + } - + } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs index f5c0e0e616ca..d2fa98ef1247 100644 --- a/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs +++ b/src/Umbraco.Core/Persistence/PetaPocoSqlExtensions.cs @@ -29,7 +29,7 @@ public static Sql From(this Sql sql, ISqlSyntaxProvider sqlSyntax) public static Sql Where(this Sql sql, Expression> predicate) { - var expresionist = new PocoToSqlExpressionHelper(); + var expresionist = new PocoToSqlExpressionVisitor(); var whereExpression = expresionist.Visit(predicate); return sql.Where(whereExpression, expresionist.GetSqlParameters()); } diff --git a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs new file mode 100644 index 000000000000..b0479a311af5 --- /dev/null +++ b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence +{ + /// + /// A data reader used for reading collections of PocoData entity types + /// + /// + /// We are using a custom data reader so that tons of memory is not consumed when rebuilding this table, previously + /// we'd generate SQL insert statements, but we'd have to put all of the XML structures into memory first. Alternatively + /// we can use .net's DataTable, but this also requires putting everything into memory. By using a DataReader we don't have to + /// store every content item and it's XML structure in memory to get it into the DB, we can stream it into the db with this + /// reader. + /// + internal class PocoDataDataReader : BulkDataReader + where TSyntax : ISqlSyntaxProvider + { + private readonly MicrosoftSqlSyntaxProviderBase _sqlSyntaxProvider; + private readonly TableDefinition _tableDefinition; + private readonly Database.PocoColumn[] _readerColumns; + private readonly IEnumerator _enumerator; + private readonly ColumnDefinition[] _columnDefinitions; + private int _recordsAffected = -1; + + public PocoDataDataReader( + IEnumerable dataSource, + Database.PocoData pd, + MicrosoftSqlSyntaxProviderBase sqlSyntaxProvider) + { + if (dataSource == null) throw new ArgumentNullException("dataSource"); + if (sqlSyntaxProvider == null) throw new ArgumentNullException("sqlSyntaxProvider"); + + _tableDefinition = DefinitionFactory.GetTableDefinition(sqlSyntaxProvider, pd.type); + if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.type); + + _readerColumns = pd.Columns.Select(x => x.Value).ToArray(); + _sqlSyntaxProvider = sqlSyntaxProvider; + _enumerator = dataSource.GetEnumerator(); + _columnDefinitions = _tableDefinition.Columns.ToArray(); + + } + + protected override string SchemaName + { + get { return _tableDefinition.SchemaName; } + } + + protected override string TableName + { + get { return _tableDefinition.Name; } + } + + public override int RecordsAffected + { + get { return _recordsAffected <= 0 ? -1 : _recordsAffected; } + } + + /// + /// This will automatically add the schema rows based on the Poco table definition and the columns passed in + /// + protected override void AddSchemaTableRows() + { + //var colNames = _readerColumns.Select(x => x.ColumnName).ToArray(); + //foreach (var col in _columnDefinitions.Where(x => colNames.Contains(x.Name, StringComparer.OrdinalIgnoreCase))) + foreach (var col in _columnDefinitions) + { + var sqlDbType = SqlDbType.NVarChar; + if (col.HasSpecialDbType) + { + //get the SqlDbType from the 'special type' + switch (col.DbType) + { + case SpecialDbTypes.NTEXT: + sqlDbType = SqlDbType.NText; + break; + case SpecialDbTypes.NCHAR: + sqlDbType = SqlDbType.NChar; + break; + default: + throw new ArgumentOutOfRangeException(); + } + } + else if (col.Type.HasValue) + { + //get the SqlDbType from the DbType + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.Type.Value); + } + else + { + //get the SqlDbType from the clr type + sqlDbType = _sqlSyntaxProvider.GetSqlDbType(col.PropertyType); + } + + AddSchemaTableRow( + col.Name, + col.Size > 0 ? (int?)col.Size : null, + col.Precision > 0 ? (short?)col.Precision : null, + null, col.IsUnique, col.IsIdentity, col.IsNullable, sqlDbType, + null, null, null, null, null); + } + + } + + /// + /// Get the value from the column index for the current object + /// + /// + /// + public override object GetValue(int i) + { + if (_enumerator.Current != null) + { + return _readerColumns[i].GetValue(_enumerator.Current); + //return _columnDefinitions[i]. .GetValue(_enumerator.Current); + } + + return null; + //TODO: Or throw ? + } + + /// + /// Advance the cursor + /// + /// + public override bool Read() + { + var result = _enumerator.MoveNext(); + if (result) + { + if (_recordsAffected == -1) + { + _recordsAffected = 0; + } + _recordsAffected++; + } + return result; + } + + /// + /// Ensure the enumerator is disposed + /// + /// + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + _enumerator.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs similarity index 63% rename from src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs rename to src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs index 0960acc9e5ba..678ceb1d8ec5 100644 --- a/src/Umbraco.Core/Persistence/Querying/BaseExpressionHelper.cs +++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs @@ -1,773 +1,916 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Persistence.Querying -{ - internal abstract class BaseExpressionHelper : BaseExpressionHelper - { - protected abstract string VisitMemberAccess(MemberExpression m); - - protected internal virtual string Visit(Expression exp) - { - - if (exp == null) return string.Empty; - switch (exp.NodeType) - { - case ExpressionType.Lambda: - return VisitLambda(exp as LambdaExpression); - case ExpressionType.MemberAccess: - return VisitMemberAccess(exp as MemberExpression); - case ExpressionType.Constant: - return VisitConstant(exp as ConstantExpression); - case ExpressionType.Add: - case ExpressionType.AddChecked: - case ExpressionType.Subtract: - case ExpressionType.SubtractChecked: - case ExpressionType.Multiply: - case ExpressionType.MultiplyChecked: - case ExpressionType.Divide: - case ExpressionType.Modulo: - case ExpressionType.And: - case ExpressionType.AndAlso: - case ExpressionType.Or: - case ExpressionType.OrElse: - case ExpressionType.LessThan: - case ExpressionType.LessThanOrEqual: - case ExpressionType.GreaterThan: - case ExpressionType.GreaterThanOrEqual: - case ExpressionType.Equal: - case ExpressionType.NotEqual: - case ExpressionType.Coalesce: - case ExpressionType.ArrayIndex: - case ExpressionType.RightShift: - case ExpressionType.LeftShift: - case ExpressionType.ExclusiveOr: - return VisitBinary(exp as BinaryExpression); - case ExpressionType.Negate: - case ExpressionType.NegateChecked: - case ExpressionType.Not: - case ExpressionType.Convert: - case ExpressionType.ConvertChecked: - case ExpressionType.ArrayLength: - case ExpressionType.Quote: - case ExpressionType.TypeAs: - return VisitUnary(exp as UnaryExpression); - case ExpressionType.Parameter: - return VisitParameter(exp as ParameterExpression); - case ExpressionType.Call: - return VisitMethodCall(exp as MethodCallExpression); - case ExpressionType.New: - return VisitNew(exp as NewExpression); - case ExpressionType.NewArrayInit: - case ExpressionType.NewArrayBounds: - return VisitNewArray(exp as NewArrayExpression); - default: - return exp.ToString(); - } - } - - protected virtual string VisitLambda(LambdaExpression lambda) - { - if (lambda.Body.NodeType == ExpressionType.MemberAccess) - { - var m = lambda.Body as MemberExpression; - - if (m.Expression != null) - { - //This deals with members that are boolean (i.e. x => IsTrashed ) - string r = VisitMemberAccess(m); - SqlParameters.Add(true); - return string.Format("{0} = @{1}", r, SqlParameters.Count - 1); - - //return string.Format("{0}={1}", r, GetQuotedTrueValue()); - } - - } - return Visit(lambda.Body); - } - - protected virtual string VisitBinary(BinaryExpression b) - { - string left, right; - var operand = BindOperant(b.NodeType); - if (operand == "AND" || operand == "OR") - { - MemberExpression m = b.Left as MemberExpression; - if (m != null && m.Expression != null) - { - string r = VisitMemberAccess(m); - - SqlParameters.Add(1); - left = string.Format("{0} = @{1}", r, SqlParameters.Count - 1); - - //left = string.Format("{0}={1}", r, GetQuotedTrueValue()); - } - else - { - left = Visit(b.Left); - } - m = b.Right as MemberExpression; - if (m != null && m.Expression != null) - { - string r = VisitMemberAccess(m); - - SqlParameters.Add(1); - right = string.Format("{0} = @{1}", r, SqlParameters.Count - 1); - - //right = string.Format("{0}={1}", r, GetQuotedTrueValue()); - } - else - { - right = Visit(b.Right); - } - } - else if (operand == "=") - { - // deal with (x == true|false) - most common - var constRight = b.Right as ConstantExpression; - if (constRight != null && constRight.Type == typeof (bool)) - return ((bool) constRight.Value) ? VisitNotNot(b.Left) : VisitNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false == x) - why not - var constLeft = b.Left as ConstantExpression; - if (constLeft != null && constLeft.Type == typeof (bool)) - return ((bool) constLeft.Value) ? VisitNotNot(b.Right) : VisitNot(b.Right); - left = Visit(b.Left); - } - else if (operand == "<>") - { - // deal with (x != true|false) - most common - var constRight = b.Right as ConstantExpression; - if (constRight != null && constRight.Type == typeof(bool)) - return ((bool) constRight.Value) ? VisitNot(b.Left) : VisitNotNot(b.Left); - right = Visit(b.Right); - - // deal with (true|false != x) - why not - var constLeft = b.Left as ConstantExpression; - if (constLeft != null && constLeft.Type == typeof(bool)) - return ((bool) constLeft.Value) ? VisitNot(b.Right) : VisitNotNot(b.Right); - left = Visit(b.Left); - } - else - { - left = Visit(b.Left); - right = Visit(b.Right); - } - - if (operand == "=" && right == "null") operand = "is"; - else if (operand == "<>" && right == "null") operand = "is not"; - else if (operand == "=" || operand == "<>") - { - //if (IsTrueExpression(right)) right = GetQuotedTrueValue(); - //else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); - - //if (IsTrueExpression(left)) left = GetQuotedTrueValue(); - //else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); - - } - - switch (operand) - { - case "MOD": - case "COALESCE": - return string.Format("{0}({1},{2})", operand, left, right); - default: - return "(" + left + " " + operand + " " + right + ")"; - } - } - - protected virtual List VisitExpressionList(ReadOnlyCollection original) - { - var list = new List(); - for (int i = 0, n = original.Count; i < n; i++) - { - if (original[i].NodeType == ExpressionType.NewArrayInit || - original[i].NodeType == ExpressionType.NewArrayBounds) - { - - list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); - } - else - list.Add(Visit(original[i])); - - } - return list; - } - - protected virtual string VisitNew(NewExpression nex) - { - // TODO : check ! - var member = Expression.Convert(nex, typeof(object)); - var lambda = Expression.Lambda>(member); - try - { - var getter = lambda.Compile(); - object o = getter(); - - SqlParameters.Add(o); - return string.Format("@{0}", SqlParameters.Count - 1); - - //return GetQuotedValue(o, o.GetType()); - } - catch (InvalidOperationException) - { - // FieldName ? - List exprs = VisitExpressionList(nex.Arguments); - var r = new StringBuilder(); - foreach (Object e in exprs) - { - r.AppendFormat("{0}{1}", - r.Length > 0 ? "," : "", - e); - } - return r.ToString(); - } - - } - - protected virtual string VisitParameter(ParameterExpression p) - { - return p.Name; - } - - protected virtual string VisitConstant(ConstantExpression c) - { - if (c.Value == null) - return "null"; - - SqlParameters.Add(c.Value); - return string.Format("@{0}", SqlParameters.Count - 1); - - //if (c.Value is bool) - //{ - // object o = GetQuotedValue(c.Value, c.Value.GetType()); - // return string.Format("({0}={1})", GetQuotedTrueValue(), o); - //} - //return GetQuotedValue(c.Value, c.Value.GetType()); - } - - protected virtual string VisitUnary(UnaryExpression u) - { - switch (u.NodeType) - { - case ExpressionType.Not: - return VisitNot(u.Operand); - default: - return Visit(u.Operand); - } - } - - private string VisitNot(Expression exp) - { - var o = Visit(exp); - - // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers - // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") - - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // false property , i.e. x => !Trashed - SqlParameters.Add(true); - return string.Format("NOT ({0} = @{1})", o, SqlParameters.Count - 1); - default: - // could be anything else, such as: x => !x.Path.StartsWith("-20") - return "NOT (" + o + ")"; - } - } - - private string VisitNotNot(Expression exp) - { - var o = Visit(exp); - - switch (exp.NodeType) - { - case ExpressionType.MemberAccess: - // true property, i.e. x => Trashed - SqlParameters.Add(true); - return string.Format("({0} = @{1})", o, SqlParameters.Count - 1); - default: - // could be anything else, such as: x => x.Path.StartsWith("-20") - return o; - } - } - - protected virtual string VisitNewArray(NewArrayExpression na) - { - - List exprs = VisitExpressionList(na.Expressions); - var r = new StringBuilder(); - foreach (Object e in exprs) - { - r.Append(r.Length > 0 ? "," + e : e); - } - - return r.ToString(); - } - - protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression na) - { - - List exprs = VisitExpressionList(na.Expressions); - return exprs; - } - - protected virtual string BindOperant(ExpressionType e) - { - - switch (e) - { - case ExpressionType.Equal: - return "="; - case ExpressionType.NotEqual: - return "<>"; - case ExpressionType.GreaterThan: - return ">"; - case ExpressionType.GreaterThanOrEqual: - return ">="; - case ExpressionType.LessThan: - return "<"; - case ExpressionType.LessThanOrEqual: - return "<="; - case ExpressionType.AndAlso: - return "AND"; - case ExpressionType.OrElse: - return "OR"; - case ExpressionType.Add: - return "+"; - case ExpressionType.Subtract: - return "-"; - case ExpressionType.Multiply: - return "*"; - case ExpressionType.Divide: - return "/"; - case ExpressionType.Modulo: - return "MOD"; - case ExpressionType.Coalesce: - return "COALESCE"; - default: - return e.ToString(); - } - } - - protected virtual string VisitMethodCall(MethodCallExpression m) - { - //Here's what happens with a MethodCallExpression: - // If a method is called that contains a single argument, - // then m.Object is the object on the left hand side of the method call, example: - // x.Path.StartsWith(content.Path) - // m.Object = x.Path - // and m.Arguments.Length == 1, therefor m.Arguments[0] == content.Path - // If a method is called that contains multiple arguments, then m.Object == null and the - // m.Arguments collection contains the left hand side of the method call, example: - // x.Path.SqlStartsWith(content.Path, TextColumnType.NVarchar) - // m.Object == null - // m.Arguments.Length == 3, therefor, m.Arguments[0] == x.Path, m.Arguments[1] == content.Path, m.Arguments[2] == TextColumnType.NVarchar - // So, we need to cater for these scenarios. - - var objectForMethod = m.Object ?? m.Arguments[0]; - var visitedObjectForMethod = Visit(objectForMethod); - var methodArgs = m.Object == null - ? m.Arguments.Skip(1).ToArray() - : m.Arguments.ToArray(); - - switch (m.Method.Name) - { - case "ToString": - SqlParameters.Add(objectForMethod.ToString()); - return string.Format("@{0}", SqlParameters.Count - 1); - case "ToUpper": - return string.Format("upper({0})", visitedObjectForMethod); - case "ToLower": - return string.Format("lower({0})", visitedObjectForMethod); - case "SqlWildcard": - case "StartsWith": - case "EndsWith": - case "Contains": - case "Equals": - case "SqlStartsWith": - case "SqlEndsWith": - case "SqlContains": - case "SqlEquals": - case "InvariantStartsWith": - case "InvariantEndsWith": - case "InvariantContains": - case "InvariantEquals": - - string compareValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - compareValue = getter().ToString(); - } - else - { - compareValue = methodArgs[0].ToString(); - } - - //special case, if it is 'Contains' and the member that Contains is being called on is not a string, then - // we should be doing an 'In' clause - but we currently do not support this - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - //default column type - var colType = TextColumnType.NVarchar; - - //then check if the col type argument has been passed to the current method (this will be the case for methods like - // SqlContains and other Sql methods) - if (methodArgs.Length > 1) - { - var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); - if (colTypeArg != null) - { - colType = (TextColumnType)((ConstantExpression)colTypeArg).Value; - } - } - - return HandleStringComparison(visitedObjectForMethod, compareValue, m.Method.Name, colType); - - case "Replace": - string searchValue; - - if (methodArgs[0].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[0], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - searchValue = getter().ToString(); - } - else - { - searchValue = methodArgs[0].ToString(); - } - - if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - string replaceValue; - - if (methodArgs[1].NodeType != ExpressionType.Constant) - { - //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) - // So we'll go get the value: - var member = Expression.Convert(methodArgs[1], typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - replaceValue = getter().ToString(); - } - else - { - replaceValue = methodArgs[1].ToString(); - } - - if (methodArgs[1].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) - { - throw new NotSupportedException("An array Contains method is not supported"); - } - - SqlParameters.Add(RemoveQuote(searchValue)); - - SqlParameters.Add(RemoveQuote(replaceValue)); - - return string.Format("replace({0}, @{1}, @{2})", visitedObjectForMethod, SqlParameters.Count - 2, SqlParameters.Count - 1); - //case "Substring": - // var startIndex = Int32.Parse(args[0].ToString()) + 1; - // if (args.Count == 2) - // { - // var length = Int32.Parse(args[1].ToString()); - // return string.Format("substring({0} from {1} for {2})", - // r, - // startIndex, - // length); - // } - // else - // return string.Format("substring({0} from {1})", - // r, - // startIndex); - //case "Round": - //case "Floor": - //case "Ceiling": - //case "Coalesce": - //case "Abs": - //case "Sum": - // return string.Format("{0}({1}{2})", - // m.Method.Name, - // r, - // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); - //case "Concat": - // var s = new StringBuilder(); - // foreach (Object e in args) - // { - // s.AppendFormat(" || {0}", e); - // } - // return string.Format("{0}{1}", r, s); - - //case "In": - - // var member = Expression.Convert(m.Arguments[0], typeof(object)); - // var lambda = Expression.Lambda>(member); - // var getter = lambda.Compile(); - - // var inArgs = (object[])getter(); - - // var sIn = new StringBuilder(); - // foreach (var e in inArgs) - // { - // SqlParameters.Add(e); - - // sIn.AppendFormat("{0}{1}", - // sIn.Length > 0 ? "," : "", - // string.Format("@{0}", SqlParameters.Count - 1)); - - // //sIn.AppendFormat("{0}{1}", - // // sIn.Length > 0 ? "," : "", - // // GetQuotedValue(e, e.GetType())); - // } - - // return string.Format("{0} {1} ({2})", r, m.Method.Name, sIn.ToString()); - //case "Desc": - // return string.Format("{0} DESC", r); - //case "Alias": - //case "As": - // return string.Format("{0} As {1}", r, - // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); - - default: - - throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); - - //var s2 = new StringBuilder(); - //foreach (Object e in args) - //{ - // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); - //} - //return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); - } - } - - public virtual string GetQuotedTableName(string tableName) - { - return string.Format("\"{0}\"", tableName); - } - - public virtual string GetQuotedColumnName(string columnName) - { - return string.Format("\"{0}\"", columnName); - } - - public virtual string GetQuotedName(string name) - { - return string.Format("\"{0}\"", name); - } - - //private string GetQuotedTrueValue() - //{ - // return GetQuotedValue(true, typeof(bool)); - //} - - //private string GetQuotedFalseValue() - //{ - // return GetQuotedValue(false, typeof(bool)); - //} - - //public virtual string GetQuotedValue(object value, Type fieldType) - //{ - // return GetQuotedValue(value, fieldType, EscapeParam, ShouldQuoteValue); - //} - - //private string GetTrueExpression() - //{ - // object o = GetQuotedTrueValue(); - // return string.Format("({0}={1})", o, o); - //} - - //private string GetFalseExpression() - //{ - - // return string.Format("({0}={1})", - // GetQuotedTrueValue(), - // GetQuotedFalseValue()); - //} - - //private bool IsTrueExpression(string exp) - //{ - // return (exp == GetTrueExpression()); - //} - - //private bool IsFalseExpression(string exp) - //{ - // return (exp == GetFalseExpression()); - //} - } - - /// - /// Logic that is shared with the expression helpers - /// - internal class BaseExpressionHelper - { - protected List SqlParameters = new List(); - - public object[] GetSqlParameters() - { - return SqlParameters.ToArray(); - } - - protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) - { - switch (verb) - { - case "SqlWildcard": - SqlParameters.Add(RemoveQuote(val)); - return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - case "Equals": - SqlParameters.Add(RemoveQuote(val)); - return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); - case "StartsWith": - SqlParameters.Add(string.Format("{0}{1}", - RemoveQuote(val), - SqlSyntaxContext.SqlSyntaxProvider.GetWildcardPlaceholder())); - return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - case "EndsWith": - SqlParameters.Add(string.Format("{0}{1}", - SqlSyntaxContext.SqlSyntaxProvider.GetWildcardPlaceholder(), - RemoveQuote(val))); - return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - case "Contains": - SqlParameters.Add(string.Format("{0}{1}{0}", - SqlSyntaxContext.SqlSyntaxProvider.GetWildcardPlaceholder(), - RemoveQuote(val))); - return SqlSyntaxContext.SqlSyntaxProvider.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); - case "InvariantEquals": - case "SqlEquals": - //recurse - return HandleStringComparison(col, val, "Equals", columnType); - case "InvariantStartsWith": - case "SqlStartsWith": - //recurse - return HandleStringComparison(col, val, "StartsWith", columnType); - case "InvariantEndsWith": - case "SqlEndsWith": - //recurse - return HandleStringComparison(col, val, "EndsWith", columnType); - case "InvariantContains": - case "SqlContains": - //recurse - return HandleStringComparison(col, val, "Contains", columnType); - default: - throw new ArgumentOutOfRangeException("verb"); - } - } - - //public virtual string GetQuotedValue(object value, Type fieldType, Func escapeCallback = null, Func shouldQuoteCallback = null) - //{ - // if (value == null) return "NULL"; - - // if (escapeCallback == null) - // { - // escapeCallback = EscapeParam; - // } - // if (shouldQuoteCallback == null) - // { - // shouldQuoteCallback = ShouldQuoteValue; - // } - - // if (!fieldType.UnderlyingSystemType.IsValueType && fieldType != typeof(string)) - // { - // //if (TypeSerializer.CanCreateFromString(fieldType)) - // //{ - // // return "'" + escapeCallback(TypeSerializer.SerializeToString(value)) + "'"; - // //} - - // throw new NotSupportedException( - // string.Format("Property of type: {0} is not supported", fieldType.FullName)); - // } - - // if (fieldType == typeof(int)) - // return ((int)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(float)) - // return ((float)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(double)) - // return ((double)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(decimal)) - // return ((decimal)value).ToString(CultureInfo.InvariantCulture); - - // if (fieldType == typeof(DateTime)) - // { - // return "'" + escapeCallback(((DateTime)value).ToIsoString()) + "'"; - // } - - // if (fieldType == typeof(bool)) - // return ((bool)value) ? Convert.ToString(1, CultureInfo.InvariantCulture) : Convert.ToString(0, CultureInfo.InvariantCulture); - - // return shouldQuoteCallback(fieldType) - // ? "'" + escapeCallback(value) + "'" - // : value.ToString(); - //} - - public virtual string EscapeParam(object paramValue) - { - return paramValue == null - ? string.Empty - : SqlSyntaxContext.SqlSyntaxProvider.EscapeString(paramValue.ToString()); - } - - public virtual bool ShouldQuoteValue(Type fieldType) - { - return true; - } - - protected virtual string RemoveQuote(string exp) - { - //if (exp.StartsWith("'") && exp.EndsWith("'")) - //{ - // exp = exp.Remove(0, 1); - // exp = exp.Remove(exp.Length - 1, 1); - //} - //return exp; - - if ((exp.StartsWith("\"") || exp.StartsWith("`") || exp.StartsWith("'")) - && - (exp.EndsWith("\"") || exp.EndsWith("`") || exp.EndsWith("'"))) - { - exp = exp.Remove(0, 1); - exp = exp.Remove(exp.Length - 1, 1); - } - return exp; - } - - //protected virtual string RemoveQuoteFromAlias(string exp) - //{ - - // if ((exp.StartsWith("\"") || exp.StartsWith("`") || exp.StartsWith("'")) - // && - // (exp.EndsWith("\"") || exp.EndsWith("`") || exp.EndsWith("'"))) - // { - // exp = exp.Remove(0, 1); - // exp = exp.Remove(exp.Length - 1, 1); - // } - // return exp; - //} - } +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// Represents an expression which caches the visitor's result. + /// + internal class CachedExpression : Expression + { + private string _visitResult; + + /// + /// Gets or sets the inner Expression. + /// + public Expression InnerExpression { get; private set; } + + /// + /// Gets or sets the compiled SQL statement output. + /// + public string VisitResult + { + get { return _visitResult; } + set + { + if (Visited) + throw new InvalidOperationException("Cached expression has already been visited."); + _visitResult = value; + Visited = true; + } + } + + /// + /// Gets or sets a value indicating whether the cache Expression has been compiled already. + /// + public bool Visited { get; private set; } + + /// + /// Replaces the inner expression. + /// + /// expression. + /// The new expression is assumed to have different parameter but produce the same SQL statement. + public void Wrap(Expression expression) + { + InnerExpression = expression; + } + } + + /// + /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression. + /// + /// This object is stateful and cannot be re-used to parse an expression. + internal abstract class ExpressionVisitorBase + { + protected ExpressionVisitorBase(ISqlSyntaxProvider sqlSyntax) + { + SqlSyntax = sqlSyntax; + } + + /// + /// Gets or sets a value indicating whether the visited expression has been visited already, + /// in which case visiting will just populate the SQL parameters. + /// + protected bool Visited { get; set; } + + /// + /// Gets or sets the SQL syntax provider for the current database. + /// + protected ISqlSyntaxProvider SqlSyntax { get; private set; } + + /// + /// Gets the list of SQL parameters. + /// + protected readonly List SqlParameters = new List(); + + /// + /// Gets the SQL parameters. + /// + /// + public object[] GetSqlParameters() + { + return SqlParameters.ToArray(); + } + + /// + /// Visits the expression and produces the corresponding SQL statement. + /// + /// The expression + /// The SQL statement corresponding to the expression. + /// Also populates the SQL parameters. + public virtual string Visit(Expression expression) + { + // if the expression is a CachedExpression, + // visit the inner expression if not already visited + var cachedExpression = expression as CachedExpression; + if (cachedExpression != null) + { + Visited = cachedExpression.Visited; + expression = cachedExpression.InnerExpression; + } + + if (expression == null) return string.Empty; + + string result; + + switch (expression.NodeType) + { + case ExpressionType.Lambda: + result = VisitLambda(expression as LambdaExpression); + break; + case ExpressionType.MemberAccess: + result = VisitMemberAccess(expression as MemberExpression); + break; + case ExpressionType.Constant: + result = VisitConstant(expression as ConstantExpression); + break; + case ExpressionType.Add: + case ExpressionType.AddChecked: + case ExpressionType.Subtract: + case ExpressionType.SubtractChecked: + case ExpressionType.Multiply: + case ExpressionType.MultiplyChecked: + case ExpressionType.Divide: + case ExpressionType.Modulo: + case ExpressionType.And: + case ExpressionType.AndAlso: + case ExpressionType.Or: + case ExpressionType.OrElse: + case ExpressionType.LessThan: + case ExpressionType.LessThanOrEqual: + case ExpressionType.GreaterThan: + case ExpressionType.GreaterThanOrEqual: + case ExpressionType.Equal: + case ExpressionType.NotEqual: + case ExpressionType.Coalesce: + case ExpressionType.ArrayIndex: + case ExpressionType.RightShift: + case ExpressionType.LeftShift: + case ExpressionType.ExclusiveOr: + result = VisitBinary(expression as BinaryExpression); + break; + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + case ExpressionType.Not: + case ExpressionType.Convert: + case ExpressionType.ConvertChecked: + case ExpressionType.ArrayLength: + case ExpressionType.Quote: + case ExpressionType.TypeAs: + result = VisitUnary(expression as UnaryExpression); + break; + case ExpressionType.Parameter: + result = VisitParameter(expression as ParameterExpression); + break; + case ExpressionType.Call: + result = VisitMethodCall(expression as MethodCallExpression); + break; + case ExpressionType.New: + result = VisitNew(expression as NewExpression); + break; + case ExpressionType.NewArrayInit: + case ExpressionType.NewArrayBounds: + result = VisitNewArray(expression as NewArrayExpression); + break; + default: + result = expression.ToString(); + break; + } + + // if the expression is a CachedExpression, + // and is not already compiled, assign the result + if (cachedExpression != null) + { + if (cachedExpression.Visited == false) + cachedExpression.VisitResult = result; + result = cachedExpression.VisitResult; + } + + return result; + } + + protected abstract string VisitMemberAccess(MemberExpression m); + + protected virtual string VisitLambda(LambdaExpression lambda) + { + if (lambda.Body.NodeType == ExpressionType.MemberAccess) + { + var m = lambda.Body as MemberExpression; + + if (m != null && m.Expression != null) + { + //This deals with members that are boolean (i.e. x => IsTrashed ) + var r = VisitMemberAccess(m); + + SqlParameters.Add(true); + + return Visited ? string.Empty : string.Format("{0} = @{1}", r, SqlParameters.Count - 1); + } + + } + return Visit(lambda.Body); + } + + protected virtual string VisitBinary(BinaryExpression b) + { + var left = string.Empty; + var right = string.Empty; + + var operand = BindOperant(b.NodeType); + if (operand == "AND" || operand == "OR") + { + var m = b.Left as MemberExpression; + if (m != null && m.Expression != null) + { + string r = VisitMemberAccess(m); + + SqlParameters.Add(1); + + //don't execute if compiled + if (Visited == false) + { + left = string.Format("{0} = @{1}", r, SqlParameters.Count - 1); + } + } + else + { + left = Visit(b.Left); + } + m = b.Right as MemberExpression; + if (m != null && m.Expression != null) + { + var r = VisitMemberAccess(m); + + SqlParameters.Add(1); + + //don't execute if compiled + if (Visited == false) + { + right = string.Format("{0} = @{1}", r, SqlParameters.Count - 1); + } + } + else + { + right = Visit(b.Right); + } + } + else if (operand == "=") + { + // deal with (x == true|false) - most common + var constRight = b.Right as ConstantExpression; + if (constRight != null && constRight.Type == typeof(bool)) + return (bool)constRight.Value ? VisitNotNot(b.Left) : VisitNot(b.Left); + right = Visit(b.Right); + + // deal with (true|false == x) - why not + var constLeft = b.Left as ConstantExpression; + if (constLeft != null && constLeft.Type == typeof(bool)) + return (bool)constLeft.Value ? VisitNotNot(b.Right) : VisitNot(b.Right); + left = Visit(b.Left); + } + else if (operand == "<>") + { + // deal with (x != true|false) - most common + var constRight = b.Right as ConstantExpression; + if (constRight != null && constRight.Type == typeof(bool)) + return (bool)constRight.Value ? VisitNot(b.Left) : VisitNotNot(b.Left); + right = Visit(b.Right); + + // deal with (true|false != x) - why not + var constLeft = b.Left as ConstantExpression; + if (constLeft != null && constLeft.Type == typeof(bool)) + return (bool)constLeft.Value ? VisitNot(b.Right) : VisitNotNot(b.Right); + left = Visit(b.Left); + } + else + { + left = Visit(b.Left); + right = Visit(b.Right); + } + + if (operand == "=" && right == "null") operand = "is"; + else if (operand == "<>" && right == "null") operand = "is not"; + else if (operand == "=" || operand == "<>") + { + //if (IsTrueExpression(right)) right = GetQuotedTrueValue(); + //else if (IsFalseExpression(right)) right = GetQuotedFalseValue(); + + //if (IsTrueExpression(left)) left = GetQuotedTrueValue(); + //else if (IsFalseExpression(left)) left = GetQuotedFalseValue(); + + } + + switch (operand) + { + case "MOD": + case "COALESCE": + //don't execute if compiled + if (Visited == false) + { + return string.Format("{0}({1},{2})", operand, left, right); + } + //already compiled, return + return string.Empty; + default: + //don't execute if compiled + if (Visited == false) + { + return string.Concat("(", left, " ", operand, " ", right, ")"); + } + //already compiled, return + return string.Empty; + } + } + + protected virtual List VisitExpressionList(ReadOnlyCollection original) + { + var list = new List(); + for (int i = 0, n = original.Count; i < n; i++) + { + if (original[i].NodeType == ExpressionType.NewArrayInit || + original[i].NodeType == ExpressionType.NewArrayBounds) + { + list.AddRange(VisitNewArrayFromExpressionList(original[i] as NewArrayExpression)); + } + else + { + list.Add(Visit(original[i])); + } + } + return list; + } + + protected virtual string VisitNew(NewExpression nex) + { + // TODO : check ! + var member = Expression.Convert(nex, typeof(object)); + var lambda = Expression.Lambda>(member); + try + { + var getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + //don't execute if compiled + if (Visited == false) + { + return string.Format("@{0}", SqlParameters.Count - 1); + } + //already compiled, return + return string.Empty; + } + catch (InvalidOperationException) + { + //don't execute if compiled + if (Visited == false) + { + // FieldName ? + List exprs = VisitExpressionList(nex.Arguments); + var r = new StringBuilder(); + foreach (var e in exprs) + { + r.AppendFormat("{0}{1}", + r.Length > 0 ? "," : "", + e); + } + return r.ToString(); + } + //already compiled, return + return string.Empty; + } + } + + protected virtual string VisitParameter(ParameterExpression p) + { + return p.Name; + } + + protected virtual string VisitConstant(ConstantExpression c) + { + if (c.Value == null) + return "null"; + + SqlParameters.Add(c.Value); + + //don't execute if compiled + if (Visited == false) + { + return string.Format("@{0}", SqlParameters.Count - 1); + } + //already compiled, return + return string.Empty; + } + + protected virtual string VisitUnary(UnaryExpression u) + { + switch (u.NodeType) + { + case ExpressionType.Not: + return VisitNot(u.Operand); + default: + return Visit(u.Operand); + } + } + + private string VisitNot(Expression exp) + { + var o = Visit(exp); + + // use a "NOT (...)" syntax instead of "<>" since we don't know whether "<>" works in all sql servers + // also, x.StartsWith(...) translates to "x LIKE '...%'" which we cannot "<>" and have to "NOT (...") + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // false property , i.e. x => !Trashed + SqlParameters.Add(true); + //don't execute if compiled + if (Visited == false) + { + return string.Format("NOT ({0} = @{1})", o, SqlParameters.Count - 1); + } + //already compiled, return + return string.Empty; + default: + //don't execute if compiled + if (Visited == false) + { + // could be anything else, such as: x => !x.Path.StartsWith("-20") + return string.Concat("NOT (", o, ")"); + } + //already compiled, return + return string.Empty; + } + } + + private string VisitNotNot(Expression exp) + { + var o = Visit(exp); + + switch (exp.NodeType) + { + case ExpressionType.MemberAccess: + // true property, i.e. x => Trashed + SqlParameters.Add(true); + + //don't execute if compiled + if (Visited == false) + { + return string.Format("({0} = @{1})", o, SqlParameters.Count - 1); + } + //already compiled, return + return string.Empty; + default: + // could be anything else, such as: x => x.Path.StartsWith("-20") + return o; + } + } + + protected virtual string VisitNewArray(NewArrayExpression na) + { + var exprs = VisitExpressionList(na.Expressions); + + //don't execute if compiled + if (Visited == false) + { + var r = new StringBuilder(); + foreach (var e in exprs) + { + r.Append(r.Length > 0 ? "," + e : e); + } + + return r.ToString(); + } + //already compiled, return + return string.Empty; + } + + protected virtual List VisitNewArrayFromExpressionList(NewArrayExpression na) + { + var exprs = VisitExpressionList(na.Expressions); + return exprs; + } + + protected virtual string BindOperant(ExpressionType e) + { + switch (e) + { + case ExpressionType.Equal: + return "="; + case ExpressionType.NotEqual: + return "<>"; + case ExpressionType.GreaterThan: + return ">"; + case ExpressionType.GreaterThanOrEqual: + return ">="; + case ExpressionType.LessThan: + return "<"; + case ExpressionType.LessThanOrEqual: + return "<="; + case ExpressionType.AndAlso: + return "AND"; + case ExpressionType.OrElse: + return "OR"; + case ExpressionType.Add: + return "+"; + case ExpressionType.Subtract: + return "-"; + case ExpressionType.Multiply: + return "*"; + case ExpressionType.Divide: + return "/"; + case ExpressionType.Modulo: + return "MOD"; + case ExpressionType.Coalesce: + return "COALESCE"; + default: + return e.ToString(); + } + } + + protected virtual string VisitMethodCall(MethodCallExpression m) + { + //Here's what happens with a MethodCallExpression: + // If a method is called that contains a single argument, + // then m.Object is the object on the left hand side of the method call, example: + // x.Path.StartsWith(content.Path) + // m.Object = x.Path + // and m.Arguments.Length == 1, therefor m.Arguments[0] == content.Path + // If a method is called that contains multiple arguments, then m.Object == null and the + // m.Arguments collection contains the left hand side of the method call, example: + // x.Path.SqlStartsWith(content.Path, TextColumnType.NVarchar) + // m.Object == null + // m.Arguments.Length == 3, therefor, m.Arguments[0] == x.Path, m.Arguments[1] == content.Path, m.Arguments[2] == TextColumnType.NVarchar + // So, we need to cater for these scenarios. + + var objectForMethod = m.Object ?? m.Arguments[0]; + var visitedObjectForMethod = Visit(objectForMethod); + var methodArgs = m.Object == null + ? m.Arguments.Skip(1).ToArray() + : m.Arguments.ToArray(); + + switch (m.Method.Name) + { + case "ToString": + SqlParameters.Add(objectForMethod.ToString()); + //don't execute if compiled + if (Visited == false) + return string.Format("@{0}", SqlParameters.Count - 1); + //already compiled, return + return string.Empty; + case "ToUpper": + //don't execute if compiled + if (Visited == false) + return string.Format("upper({0})", visitedObjectForMethod); + //already compiled, return + return string.Empty; + case "ToLower": + //don't execute if compiled + if (Visited == false) + return string.Format("lower({0})", visitedObjectForMethod); + //already compiled, return + return string.Empty; + case "SqlWildcard": + case "StartsWith": + case "EndsWith": + case "Contains": + case "Equals": + case "SqlStartsWith": + case "SqlEndsWith": + case "SqlContains": + case "SqlEquals": + case "InvariantStartsWith": + case "InvariantEndsWith": + case "InvariantContains": + case "InvariantEquals": + + string compareValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + var member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + compareValue = getter().ToString(); + } + else + { + compareValue = methodArgs[0].ToString(); + } + + //special case, if it is 'Contains' and the member that Contains is being called on is not a string, then + // we should be doing an 'In' clause - but we currently do not support this + if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + //default column type + var colType = TextColumnType.NVarchar; + + //then check if the col type argument has been passed to the current method (this will be the case for methods like + // SqlContains and other Sql methods) + if (methodArgs.Length > 1) + { + var colTypeArg = methodArgs.FirstOrDefault(x => x is ConstantExpression && x.Type == typeof(TextColumnType)); + if (colTypeArg != null) + { + colType = (TextColumnType)((ConstantExpression)colTypeArg).Value; + } + } + + return HandleStringComparison(visitedObjectForMethod, compareValue, m.Method.Name, colType); + + case "Replace": + string searchValue; + + if (methodArgs[0].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + var member = Expression.Convert(methodArgs[0], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + searchValue = getter().ToString(); + } + else + { + searchValue = methodArgs[0].ToString(); + } + + if (methodArgs[0].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[0].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + string replaceValue; + + if (methodArgs[1].NodeType != ExpressionType.Constant) + { + //This occurs when we are getting a value from a non constant such as: x => x.Path.StartsWith(content.Path) + // So we'll go get the value: + var member = Expression.Convert(methodArgs[1], typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + replaceValue = getter().ToString(); + } + else + { + replaceValue = methodArgs[1].ToString(); + } + + if (methodArgs[1].Type != typeof(string) && TypeHelper.IsTypeAssignableFrom(methodArgs[1].Type)) + { + throw new NotSupportedException("An array Contains method is not supported"); + } + + SqlParameters.Add(RemoveQuote(searchValue)); + + SqlParameters.Add(RemoveQuote(replaceValue)); + + //don't execute if compiled + if (Visited == false) + return string.Format("replace({0}, @{1}, @{2})", visitedObjectForMethod, SqlParameters.Count - 2, SqlParameters.Count - 1); + //already compiled, return + return string.Empty; + + //case "Substring": + // var startIndex = Int32.Parse(args[0].ToString()) + 1; + // if (args.Count == 2) + // { + // var length = Int32.Parse(args[1].ToString()); + // return string.Format("substring({0} from {1} for {2})", + // r, + // startIndex, + // length); + // } + // else + // return string.Format("substring({0} from {1})", + // r, + // startIndex); + //case "Round": + //case "Floor": + //case "Ceiling": + //case "Coalesce": + //case "Abs": + //case "Sum": + // return string.Format("{0}({1}{2})", + // m.Method.Name, + // r, + // args.Count == 1 ? string.Format(",{0}", args[0]) : ""); + //case "Concat": + // var s = new StringBuilder(); + // foreach (Object e in args) + // { + // s.AppendFormat(" || {0}", e); + // } + // return string.Format("{0}{1}", r, s); + + //case "In": + + // var member = Expression.Convert(m.Arguments[0], typeof(object)); + // var lambda = Expression.Lambda>(member); + // var getter = lambda.Compile(); + + // var inArgs = (object[])getter(); + + // var sIn = new StringBuilder(); + // foreach (var e in inArgs) + // { + // SqlParameters.Add(e); + + // sIn.AppendFormat("{0}{1}", + // sIn.Length > 0 ? "," : "", + // string.Format("@{0}", SqlParameters.Count - 1)); + + // //sIn.AppendFormat("{0}{1}", + // // sIn.Length > 0 ? "," : "", + // // GetQuotedValue(e, e.GetType())); + // } + + // return string.Format("{0} {1} ({2})", r, m.Method.Name, sIn.ToString()); + //case "Desc": + // return string.Format("{0} DESC", r); + //case "Alias": + //case "As": + // return string.Format("{0} As {1}", r, + // GetQuotedColumnName(RemoveQuoteFromAlias(RemoveQuote(args[0].ToString())))); + + default: + + throw new ArgumentOutOfRangeException("No logic supported for " + m.Method.Name); + + //var s2 = new StringBuilder(); + //foreach (Object e in args) + //{ + // s2.AppendFormat(",{0}", GetQuotedValue(e, e.GetType())); + //} + //return string.Format("{0}({1}{2})", m.Method.Name, r, s2.ToString()); + } + } + + public virtual string GetQuotedTableName(string tableName) + { + return Visited ? tableName : string.Format("\"{0}\"", tableName); + } + + public virtual string GetQuotedColumnName(string columnName) + { + return Visited ? columnName : string.Format("\"{0}\"", columnName); + } + + public virtual string GetQuotedName(string name) + { + return Visited ? name : string.Format("\"{0}\"", name); + } + + protected string HandleStringComparison(string col, string val, string verb, TextColumnType columnType) + { + switch (verb) + { + case "SqlWildcard": + SqlParameters.Add(RemoveQuote(val)); + //don't execute if compiled + if (Visited == false) + return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + //already compiled, return + return string.Empty; + case "Equals": + SqlParameters.Add(RemoveQuote(val)); + //don't execute if compiled + if (Visited == false) + return SqlSyntax.GetStringColumnEqualComparison(col, SqlParameters.Count - 1, columnType); + //already compiled, return + return string.Empty; + case "StartsWith": + SqlParameters.Add(string.Format("{0}{1}", + RemoveQuote(val), + SqlSyntax.GetWildcardPlaceholder())); + //don't execute if compiled + if (Visited == false) + return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + //already compiled, return + return string.Empty; + case "EndsWith": + SqlParameters.Add(string.Format("{0}{1}", + SqlSyntax.GetWildcardPlaceholder(), + RemoveQuote(val))); + //don't execute if compiled + if (Visited == false) + return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + //already compiled, return + return string.Empty; + case "Contains": + SqlParameters.Add(string.Format("{0}{1}{0}", + SqlSyntax.GetWildcardPlaceholder(), + RemoveQuote(val))); + //don't execute if compiled + if (Visited == false) + return SqlSyntax.GetStringColumnWildcardComparison(col, SqlParameters.Count - 1, columnType); + //already compiled, return + return string.Empty; + case "InvariantEquals": + case "SqlEquals": + //recurse + return HandleStringComparison(col, val, "Equals", columnType); + case "InvariantStartsWith": + case "SqlStartsWith": + //recurse + return HandleStringComparison(col, val, "StartsWith", columnType); + case "InvariantEndsWith": + case "SqlEndsWith": + //recurse + return HandleStringComparison(col, val, "EndsWith", columnType); + case "InvariantContains": + case "SqlContains": + //recurse + return HandleStringComparison(col, val, "Contains", columnType); + default: + throw new ArgumentOutOfRangeException("verb"); + } + } + + //public virtual string GetQuotedValue(object value, Type fieldType, Func escapeCallback = null, Func shouldQuoteCallback = null) + //{ + // if (value == null) return "NULL"; + + // if (escapeCallback == null) + // { + // escapeCallback = EscapeParam; + // } + // if (shouldQuoteCallback == null) + // { + // shouldQuoteCallback = ShouldQuoteValue; + // } + + // if (!fieldType.UnderlyingSystemType.IsValueType && fieldType != typeof(string)) + // { + // //if (TypeSerializer.CanCreateFromString(fieldType)) + // //{ + // // return "'" + escapeCallback(TypeSerializer.SerializeToString(value)) + "'"; + // //} + + // throw new NotSupportedException( + // string.Format("Property of type: {0} is not supported", fieldType.FullName)); + // } + + // if (fieldType == typeof(int)) + // return ((int)value).ToString(CultureInfo.InvariantCulture); + + // if (fieldType == typeof(float)) + // return ((float)value).ToString(CultureInfo.InvariantCulture); + + // if (fieldType == typeof(double)) + // return ((double)value).ToString(CultureInfo.InvariantCulture); + + // if (fieldType == typeof(decimal)) + // return ((decimal)value).ToString(CultureInfo.InvariantCulture); + + // if (fieldType == typeof(DateTime)) + // { + // return "'" + escapeCallback(((DateTime)value).ToIsoString()) + "'"; + // } + + // if (fieldType == typeof(bool)) + // return ((bool)value) ? Convert.ToString(1, CultureInfo.InvariantCulture) : Convert.ToString(0, CultureInfo.InvariantCulture); + + // return shouldQuoteCallback(fieldType) + // ? "'" + escapeCallback(value) + "'" + // : value.ToString(); + //} + + public virtual string EscapeParam(object paramValue) + { + return paramValue == null ? string.Empty : SqlSyntax.EscapeString(paramValue.ToString()); + } + + public virtual bool ShouldQuoteValue(Type fieldType) + { + return true; + } + + protected virtual string RemoveQuote(string exp) + { + if ((exp.StartsWith("\"") || exp.StartsWith("`") || exp.StartsWith("'")) + && + (exp.EndsWith("\"") || exp.EndsWith("`") || exp.EndsWith("'"))) + { + exp = exp.Remove(0, 1); + exp = exp.Remove(exp.Length - 1, 1); + } + return exp; + } + + //protected virtual string RemoveQuoteFromAlias(string expression) + //{ + + // if ((expression.StartsWith("\"") || expression.StartsWith("`") || expression.StartsWith("'")) + // && + // (expression.EndsWith("\"") || expression.EndsWith("`") || expression.EndsWith("'"))) + // { + // expression = expression.Remove(0, 1); + // expression = expression.Remove(expression.Length - 1, 1); + // } + // return expression; + //} + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/IQuery.cs b/src/Umbraco.Core/Persistence/Querying/IQuery.cs index b158943cb494..ae986baddcb9 100644 --- a/src/Umbraco.Core/Persistence/Querying/IQuery.cs +++ b/src/Umbraco.Core/Persistence/Querying/IQuery.cs @@ -1,31 +1,8 @@ using System; -using System.Collections.Generic; using System.Linq.Expressions; namespace Umbraco.Core.Persistence.Querying { - /// - /// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} there's not much we can do. - /// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. - /// We have to wait till v8 to make this change I suppose. - /// - internal static class QueryExtensions - { - /// - /// Returns all translated where clauses and their sql parameters - /// - /// - public static IEnumerable> GetWhereClauses(this IQuery query) - { - var q = query as Query; - if (q == null) - { - throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); - } - return q.GetWhereClauses(); - } - } - /// /// Represents a query for building Linq translatable SQL queries /// diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs deleted file mode 100644 index a0ccfaa07076..000000000000 --- a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionHelper.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics; -using System.Globalization; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using Umbraco.Core.Persistence.Mappers; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Persistence.Querying -{ - internal class ModelToSqlExpressionHelper : BaseExpressionHelper - { - - private readonly BaseMapper _mapper; - - public ModelToSqlExpressionHelper() - { - _mapper = MappingResolver.Current.ResolveMapperByType(typeof(T)); - } - - protected override string VisitMemberAccess(MemberExpression m) - { - if (m.Expression != null && - m.Expression.NodeType == ExpressionType.Parameter - && m.Expression.Type == typeof(T)) - { - var field = _mapper.Map(m.Member.Name, true); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); - return field; - } - - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - var field = _mapper.Map(m.Member.Name, true); - if (field.IsNullOrWhiteSpace()) - throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); - return field; - } - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - object o = getter(); - - SqlParameters.Add(o); - return string.Format("@{0}", SqlParameters.Count - 1); - - //return GetQuotedValue(o, o != null ? o.GetType() : null); - - } - - //protected bool IsFieldName(string quotedExp) - //{ - // //Not entirely sure this is reliable, but its better then simply returning true - // return quotedExp.LastIndexOf("'", StringComparison.InvariantCultureIgnoreCase) + 1 != quotedExp.Length; - //} - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs new file mode 100644 index 000000000000..b265a5b587d2 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/ModelToSqlExpressionVisitor.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq.Expressions; +using Umbraco.Core.Persistence.Mappers; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, + /// based on Umbraco's business logic models. + /// + /// This object is stateful and cannot be re-used to parse an expression. + internal class ModelToSqlExpressionVisitor : ExpressionVisitorBase + { + private readonly BaseMapper _mapper; + + public ModelToSqlExpressionVisitor(ISqlSyntaxProvider sqlSyntax, BaseMapper mapper) + : base(sqlSyntax) + { + _mapper = mapper; + } + + public ModelToSqlExpressionVisitor() + : this(SqlSyntaxContext.SqlSyntaxProvider, MappingResolver.Current.ResolveMapperByType(typeof(T))) + { } + + protected override string VisitMemberAccess(MemberExpression m) + { + if (m.Expression != null && + m.Expression.NodeType == ExpressionType.Parameter + && m.Expression.Type == typeof(T)) + { + //don't execute if compiled + if (Visited == false) + { + var field = _mapper.Map(m.Member.Name, true); + if (field.IsNullOrWhiteSpace()) + throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); + return field; + } + //already compiled, return + return string.Empty; + } + + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + { + //don't execute if compiled + if (Visited == false) + { + var field = _mapper.Map(m.Member.Name, true); + if (field.IsNullOrWhiteSpace()) + throw new InvalidOperationException("The mapper returned an empty field for the member name: " + m.Member.Name); + return field; + } + //already compiled, return + return string.Empty; + } + + var member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + //don't execute if compiled + if (Visited == false) + return string.Format("@{0}", SqlParameters.Count - 1); + //already compiled, return + return string.Empty; + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionHelper.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionHelper.cs deleted file mode 100644 index bbdb7a5509d2..000000000000 --- a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionHelper.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Linq.Expressions; -using System.Text; -using Umbraco.Core.Persistence.SqlSyntax; - -namespace Umbraco.Core.Persistence.Querying -{ - internal class PocoToSqlExpressionHelper : BaseExpressionHelper - { - private readonly Database.PocoData _pd; - - public PocoToSqlExpressionHelper() - { - _pd = new Database.PocoData(typeof(T)); - } - - protected override string VisitMemberAccess(MemberExpression m) - { - if (m.Expression != null && - m.Expression.NodeType == ExpressionType.Parameter - && m.Expression.Type == typeof(T)) - { - string field = GetFieldName(_pd, m.Member.Name); - return field; - } - - if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) - { - string field = GetFieldName(_pd, m.Member.Name); - return field; - } - - var member = Expression.Convert(m, typeof(object)); - var lambda = Expression.Lambda>(member); - var getter = lambda.Compile(); - object o = getter(); - - SqlParameters.Add(o); - return string.Format("@{0}", SqlParameters.Count - 1); - - //return GetQuotedValue(o, o != null ? o.GetType() : null); - - } - - protected virtual string GetFieldName(Database.PocoData pocoData, string name) - { - var column = pocoData.Columns.FirstOrDefault(x => x.Value.PropertyInfo.Name == name); - return string.Format("{0}.{1}", - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedTableName(pocoData.TableInfo.TableName), - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName(column.Value.ColumnName)); - } - - //protected bool IsFieldName(string quotedExp) - //{ - // return true; - //} - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs new file mode 100644 index 000000000000..6b527296dfda --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Linq.Expressions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// An expression tree parser to create SQL statements and SQL parameters based on a strongly typed expression, + /// based on Umbraco's DTOs. + /// + /// This object is stateful and cannot be re-used to parse an expression. + internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase + { + private readonly Database.PocoData _pd; + + public PocoToSqlExpressionVisitor() + : base(SqlSyntaxContext.SqlSyntaxProvider) + { + _pd = new Database.PocoData(typeof(T)); + } + + protected override string VisitMemberAccess(MemberExpression m) + { + if (m.Expression != null && + m.Expression.NodeType == ExpressionType.Parameter + && m.Expression.Type == typeof(T)) + { + //don't execute if compiled + if (Visited == false) + { + string field = GetFieldName(_pd, m.Member.Name); + return field; + } + //already compiled, return + return string.Empty; + } + + if (m.Expression != null && m.Expression.NodeType == ExpressionType.Convert) + { + //don't execute if compiled + if (Visited == false) + { + string field = GetFieldName(_pd, m.Member.Name); + return field; + } + //already compiled, return + return string.Empty; + } + + var member = Expression.Convert(m, typeof(object)); + var lambda = Expression.Lambda>(member); + var getter = lambda.Compile(); + var o = getter(); + + SqlParameters.Add(o); + + //don't execute if compiled + if (Visited == false) + return string.Format("@{0}", SqlParameters.Count - 1); + //already compiled, return + return string.Empty; + } + + protected virtual string GetFieldName(Database.PocoData pocoData, string name) + { + var column = pocoData.Columns.FirstOrDefault(x => x.Value.PropertyInfo.Name == name); + return string.Format("{0}.{1}", + SqlSyntax.GetQuotedTableName(pocoData.TableInfo.TableName), + SqlSyntax.GetQuotedColumnName(column.Value.ColumnName)); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Querying/Query.cs b/src/Umbraco.Core/Persistence/Querying/Query.cs index 4dd268268f9d..6213ca5ed69d 100644 --- a/src/Umbraco.Core/Persistence/Querying/Query.cs +++ b/src/Umbraco.Core/Persistence/Querying/Query.cs @@ -30,7 +30,8 @@ public virtual IQuery Where(Expression> predicate) { if (predicate != null) { - var expressionHelper = new ModelToSqlExpressionHelper(); + //TODO: This should have an SqlSyntax object passed in, this ctor is relying on a singleton + var expressionHelper = new ModelToSqlExpressionVisitor(); string whereExpression = expressionHelper.Visit(predicate); _wheres.Add(new Tuple(whereExpression, expressionHelper.GetSqlParameters())); diff --git a/src/Umbraco.Core/Persistence/Querying/QueryExtensions.cs b/src/Umbraco.Core/Persistence/Querying/QueryExtensions.cs new file mode 100644 index 000000000000..20c3409a4019 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Querying/QueryExtensions.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace Umbraco.Core.Persistence.Querying +{ + /// + /// SD: This is a horrible hack but unless we break compatibility with anyone who's actually implemented IQuery{T} there's not much we can do. + /// The IQuery{T} interface is useless without having a GetWhereClauses method and cannot be used for tests. + /// We have to wait till v8 to make this change I suppose. + /// + internal static class QueryExtensions + { + /// + /// Returns all translated where clauses and their sql parameters + /// + /// + public static IEnumerable> GetWhereClauses(this IQuery query) + { + var q = query as Query; + if (q == null) + { + throw new NotSupportedException(typeof(IQuery) + " cannot be cast to " + typeof(Query)); + } + return q.GetWhereClauses(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 92227692473c..d0c226ace286 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,22 +1,14 @@ using System; using System.Collections.Generic; -using System.Data; using System.Globalization; using System.Linq; -using System.Linq.Expressions; -using System.Net.Http.Headers; -using System.Text; using System.Xml; using System.Xml.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.Dynamics; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; using Umbraco.Core.Models.Rdbms; - using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; @@ -81,7 +73,7 @@ protected override IEnumerable PerformGetAll(params int[] ids) var sql = GetBaseQuery(false); if (ids.Any()) { - sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); + sql.Where("umbracoNode.id in (@ids)", new { ids }); } //we only want the newest ones with this method @@ -104,6 +96,12 @@ protected override IEnumerable PerformGetByQuery(IQuery quer #endregion + #region Static Queries + + private readonly IQuery _publishedQuery = Query.Builder.Where(x => x.Published == true); + + #endregion + #region Overrides of PetaPocoRepositoryBase @@ -173,90 +171,57 @@ protected override Guid NodeObjectTypeId #region Overrides of VersionableRepositoryBase - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) - { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) - { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) + public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) + { + // the previous way of doing this was to run it all in one big transaction, + // and to bulk-insert groups of xml rows - which works, until the transaction + // times out - and besides, because v7 transactions are ReadCommited, it does + // not bring much safety - so this reverts to updating each record individually, + // and it may be slower in the end, but should be more resilient. + + var baseId = 0; + var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + while (true) + { + // get the next group of nodes + var query = GetBaseQuery(false); + if (contentTypeIdsA.Length > 0) + query = query + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + query = query + .Where(x => x.NodeId > baseId && x.Trashed == false) + .Where(x => x.Published) + .OrderBy(x => x.NodeId, SqlSyntax); + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) + .ToList(); + + // no more nodes, break + if (xmlItems.Count == 0) break; + + foreach (var xmlItem in xmlItems) { - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) + try { - var id1 = id; - var subQuery = new Sql() - .Select("cmsDocument.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.Published) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); + // should happen in most cases, then it tries to insert, and it should work + // unless the node has been deleted, and we just report the exception + Database.InsertOrUpdate(xmlItem); } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder.Where(x => x.Published == true); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) + catch (Exception e) { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.Published == true && x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - - tr.Complete(); + baseId = xmlItems.Last().NodeId; } } - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) + public override IEnumerable GetAllVersions(int id) { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - //NOTE: This is an important call, we cannot simply make a call to: - // GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending); - // because that method is used to query 'latest' content items where in this case we don't necessarily - // want latest content items because a pulished content item might not actually be the latest. - // see: http://issues.umbraco.org/issue/U4-6322 & http://issues.umbraco.org/issue/U4-5982 - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, - new Tuple("cmsDocument", "nodeId"), - ProcessQuery, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); + var sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { Id = id }) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + return ProcessQuery(sql, true); } public override IContent GetByVersion(Guid versionId) @@ -667,29 +632,9 @@ public IEnumerable GetByPublishedVersion(IQuery query) .OrderBy(x => x.Level, SqlSyntax) .OrderBy(x => x.SortOrder, SqlSyntax); - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - foreach (var dto in dtos) - { - //Check in the cache first. If it exists there AND it is published - // then we can use that entity. Otherwise if it is not published (which can be the case - // because we only store the 'latest' entries in the cache which might not be the published - // version) - var fromCache = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); - //var fromCache = TryGetFromCache(dto.NodeId); - if (fromCache != null && fromCache.Published) - { - yield return fromCache; - } - else - { - yield return CreateContentFromDto(dto, dto.VersionId, sql); - } - } + return ProcessQuery(sql, true); } - /// /// This builds the Xml document used for the XML cache /// @@ -863,7 +808,7 @@ public IEnumerable GetPagedResultsByQuery(IQuery query, long return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - ProcessQuery, orderBy, orderDirection, orderBySystemField, + sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -894,83 +839,79 @@ protected override string GetDatabaseFieldNameForOrderBy(string orderBy) return base.GetDatabaseFieldNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql) + private IEnumerable ProcessQuery(Sql sql, bool withCache = false) { - //NOTE: This doesn't allow properties to be part of the query + // fetch returns a list so it's ok to iterate it in this method var dtos = Database.Fetch(sql); + if (dtos.Count == 0) return Enumerable.Empty(); - //nothing found - if (dtos.Any() == false) return Enumerable.Empty(); + var content = new IContent[dtos.Count]; + var defs = new List(); + var templateIds = new List(); - //content types - //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types - var contentTypes = _contentTypeRepository.GetAll(dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray()) - .ToArray(); + for (var i = 0; i < dtos.Count; i++) + { + var dto = dtos[i]; + // if the cache contains the published version, use it + if (withCache) + { + var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + if (cached != null && cached.Published) + { + content[i] = cached; + continue; + } + } - var ids = dtos - .Where(dto => dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - .Select(x => x.TemplateId.Value).ToArray(); + // else, need to fetch from the database + // content type repository is full-cache so OK to get each one independently + var contentType = _contentTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); + content[i] = factory.BuildEntity(dto); - //NOTE: This should be ok for an SQL 'IN' statement, there shouldn't be an insane amount of content types - var templates = ids.Length == 0 ? Enumerable.Empty() : _templateRepository.GetAll(ids).ToArray(); + // need template + if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) + templateIds.Add(dto.TemplateId.Value); - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); + // need properties + defs.Add(new DocumentDefinition( + dto.NodeId, + dto.VersionId, + dto.ContentVersionDto.VersionDate, + dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, + contentType + )); + } - //Go get the property data for each document - var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.VersionId, - d.dto.ContentVersionDto.VersionDate, - d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - d.contentType)); + // load all required templates in 1 query + var templates = _templateRepository.GetAll(templateIds.ToArray()) + .ToDictionary(x => x.Id, x => x); - var propertyData = GetPropertyCollection(sql, docDefs); + // load all properties for all documents from database in 1 query + var propertyData = GetPropertyCollection(sql, defs); - return dtosWithContentTypes.Select(d => CreateContentFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - templates.FirstOrDefault(tem => tem.Id == (d.dto.TemplateId.HasValue ? d.dto.TemplateId.Value : -1)), - propertyData[d.dto.NodeId])); - } + // assign + var dtoIndex = 0; + foreach (var def in defs) + { + // move to corresponding item (which has to exist) + while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; - /// - /// Private method to create a content object from a DocumentDto, which is used by Get and GetByVersion. - /// - /// - /// - /// - /// - /// - private IContent CreateContentFromDto(DocumentDto dto, - IContentType contentType, - ITemplate template, - Models.PropertyCollection propCollection) - { - var factory = new ContentFactory(contentType, NodeObjectTypeId, dto.NodeId); - var content = factory.BuildEntity(dto); + // complete the item + var cc = content[dtoIndex]; + var dto = dtos[dtoIndex]; + ITemplate template = null; + if (dto.TemplateId.HasValue) + templates.TryGetValue(dto.TemplateId.Value, out template); // else null + cc.Template = template; + cc.Properties = propertyData[cc.Id]; - //Check if template id is set on DocumentDto, and get ITemplate if it is. - if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) - { - content.Template = template ?? _templateRepository.Get(dto.TemplateId.Value); + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity) cc).ResetDirtyProperties(false); } - else - { - //ensure there isn't one set. - content.Template = null; - } - - content.Properties = propCollection; - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)content).ResetDirtyProperties(false); return content; } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 907f9b62c5d3..64989f92691a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -38,5 +38,15 @@ public interface IMediaRepository : IRepositoryVersionable, IRecycl /// An Enumerable list of objects IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, string filter = ""); + + /// + /// Gets paged media descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of media items + IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, out long totalRecords); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs index 229a6fc0efcf..3e05d1feafbb 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryVersionable.cs @@ -34,11 +34,19 @@ public interface IRepositoryVersionable : IRepositoryQueryable - /// Gets a list of all versions for an . + /// Gets a list of all versions for an ordered so latest is first /// /// Id of the to retrieve versions from /// An enumerable list of the same object with different versions - IEnumerable GetAllVersions(int id); + IEnumerable GetAllVersions(int id); + + /// + /// Gets a list of all version Ids for the given content item + /// + /// + /// The maximum number of rows to return + /// + IEnumerable GetVersionIds(int id, int maxRows); /// /// Gets a specific version of an . diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 8c9bec71d44d..092b7df0253d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -4,21 +4,17 @@ using System.Linq; using System.Text; using System.Xml.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.Dynamics; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Rdbms; - +using Umbraco.Core.Cache; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -137,6 +133,74 @@ protected override Guid NodeObjectTypeId #region Overrides of VersionableRepositoryBase + public override IEnumerable GetAllVersions(int id) + { + var sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { Id = id }) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + return ProcessQuery(sql, true); + } + + private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + { + // fetch returns a list so it's ok to iterate it in this method + var dtos = Database.Fetch(sql); + var content = new IMedia[dtos.Count]; + var defs = new List(); + + for (var i = 0; i < dtos.Count; i++) + { + var dto = dtos[i]; + + // if the cache contains the item, use it + if (withCache) + { + var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + if (cached != null) + { + content[i] = cached; + continue; + } + } + + // else, need to fetch from the database + // content type repository is full-cache so OK to get each one independently + var contentType = _mediaTypeRepository.Get(dto.ContentDto.ContentTypeId); + var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); + content[i] = factory.BuildEntity(dto); + + // need properties + defs.Add(new DocumentDefinition( + dto.NodeId, + dto.VersionId, + dto.VersionDate, + dto.ContentDto.NodeDto.CreateDate, + contentType + )); + } + + // load all properties for all documents from database in 1 query + var propertyData = GetPropertyCollection(sql, defs); + + // assign + var dtoIndex = 0; + foreach (var def in defs) + { + // move to corresponding item (which has to exist) + while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; + + // complete the item + var cc = content[dtoIndex]; + cc.Properties = propertyData[cc.Id]; + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity) cc).ResetDirtyProperties(false); + } + + return content; + } + public override IMedia GetByVersion(Guid versionId) { var sql = GetBaseQuery(false); @@ -163,90 +227,51 @@ public override IMedia GetByVersion(Guid versionId) return media; } - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) { - - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) + // the previous way of doing this was to run it all in one big transaction, + // and to bulk-insert groups of xml rows - which works, until the transaction + // times out - and besides, because v7 transactions are ReadCommited, it does + // not bring much safety - so this reverts to updating each record individually, + // and it may be slower in the end, but should be more resilient. + + var baseId = 0; + var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + while (true) { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) - { - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else + // get the next group of nodes + var query = GetBaseQuery(false); + if (contentTypeIdsA.Length > 0) + query = query + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + query = query + .Where(x => x.NodeId > baseId) + .OrderBy(x => x.NodeId, SqlSyntax); + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) + .ToList(); + + // no more nodes, break + if (xmlItems.Count == 0) break; + + foreach (var xmlItem in xmlItems) { - foreach (var id in contentTypeIds) + try { - var id1 = id; - var mediaObjectType = Guid.Parse(Constants.ObjectTypes.Media); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == mediaObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); + // InsertOrUpdate tries to update first, which is good since it is what + // should happen in most cases, then it tries to insert, and it should work + // unless the node has been deleted, and we just report the exception + Database.InsertOrUpdate(xmlItem); } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) + catch (Exception e) { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - - tr.Complete(); + baseId = xmlItems.Last().NodeId; } } - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - public void AddOrUpdateContentXml(IMedia content, Func xml) { _contentXmlRepository.AddOrUpdate(new ContentXmlEntity(content, xml)); @@ -470,65 +495,33 @@ public IEnumerable GetPagedResultsByQuery(IQuery query, long pag return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - ProcessQuery, orderBy, orderDirection, orderBySystemField, + sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, filterCallback); } - private IEnumerable ProcessQuery(Sql sql) - { - //NOTE: This doesn't allow properties to be part of the query - var dtos = Database.Fetch(sql); - - var ids = dtos.Select(x => x.ContentDto.ContentTypeId).ToArray(); - - //content types - var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _mediaTypeRepository.GetAll(ids).ToArray(); - - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); - - //Go get the property data for each document - var docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.VersionId, - d.dto.VersionDate, - d.dto.ContentDto.NodeDto.CreateDate, - d.contentType)) - .ToArray(); - - var propertyData = GetPropertyCollection(sql, docDefs); - - return dtosWithContentTypes.Select(d => CreateMediaFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); - } - /// - /// Private method to create a media object from a ContentDto + /// Gets paged media descendants as XML by path /// - /// - /// - /// - /// - private IMedia CreateMediaFromDto(ContentVersionDto dto, - IMediaType contentType, - PropertyCollection propCollection) + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of media items + public IEnumerable GetPagedXmlEntriesByPath(string path, long pageIndex, int pageSize, out long totalRecords) { - var factory = new MediaFactory(contentType, NodeObjectTypeId, dto.NodeId); - var media = factory.BuildEntity(dto); - - media.Properties = propCollection; - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)media).ResetDirtyProperties(false); - return media; + Sql query; + if (path == "-1") + { + query = new Sql().Select("nodeId, xml").From("cmsContentXml").Where("nodeId IN (SELECT id FROM umbracoNode WHERE nodeObjectType = @0)", Guid.Parse(Constants.ObjectTypes.Media)).OrderBy("nodeId"); + } + else + { + query = new Sql().Select("nodeId, xml").From("cmsContentXml").Where("nodeId IN (SELECT id FROM umbracoNode WHERE path LIKE @0)", path.EnsureEndsWith(",%")).OrderBy("nodeId"); + } + var pagedResult = Database.Page(pageIndex+1, pageSize, query); + totalRecords = pagedResult.TotalItems; + return pagedResult.Items.Select(dto => XElement.Parse(dto.Xml)); } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 0f0e797f17d0..dcab8986851c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -2,24 +2,19 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; -using System.Linq.Expressions; using System.Text; using System.Xml.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; - +using Umbraco.Core.Cache; using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; -using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; -using Umbraco.Core.Dynamics; namespace Umbraco.Core.Persistence.Repositories { @@ -380,90 +375,59 @@ protected override void PersistUpdatedItem(IMember entity) #region Overrides of VersionableRepositoryBase - public void RebuildXmlStructures(Func serializer, int groupSize = 5000, IEnumerable contentTypeIds = null) + public override IEnumerable GetAllVersions(int id) { + var sql = GetBaseQuery(false) + .Where(GetBaseWhereClause(), new { Id = id }) + .OrderByDescending(x => x.VersionDate, SqlSyntax); + return ProcessQuery(sql, true); + } - //Ok, now we need to remove the data and re-insert it, we'll do this all in one transaction too. - using (var tr = Database.GetTransaction()) + public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) + { + // the previous way of doing this was to run it all in one big transaction, + // and to bulk-insert groups of xml rows - which works, until the transaction + // times out - and besides, because v7 transactions are ReadCommited, it does + // not bring much safety - so this reverts to updating each record individually, + // and it may be slower in the end, but should be more resilient. + + var baseId = 0; + var contentTypeIdsA = contentTypeIds == null ? new int[0] : contentTypeIds.ToArray(); + while (true) { - //Remove all the data first, if anything fails after this it's no problem the transaction will be reverted - if (contentTypeIds == null) + // get the next group of nodes + var query = GetBaseQuery(false); + if (contentTypeIdsA.Length > 0) + query = query + .WhereIn(x => x.ContentTypeId, contentTypeIdsA, SqlSyntax); + query = query + .Where(x => x.NodeId > baseId) + .OrderBy(x => x.NodeId, SqlSyntax); + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) + .ToList(); + + // no more nodes, break + if (xmlItems.Count == 0) break; + + foreach (var xmlItem in xmlItems) { - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); - } - else - { - foreach (var id in contentTypeIds) + try { - var id1 = id; - var memberObjectType = Guid.Parse(Constants.ObjectTypes.Member); - var subQuery = new Sql() - .Select("DISTINCT cmsContentXml.nodeId") - .From() - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .InnerJoin() - .On(left => left.NodeId, right => right.NodeId) - .Where(dto => dto.NodeObjectType == memberObjectType) - .Where(dto => dto.ContentTypeId == id1); - - var deleteSql = SqlSyntax.GetDeleteSubquery("cmsContentXml", "nodeId", subQuery); - Database.Execute(deleteSql); + // InsertOrUpdate tries to update first, which is good since it is what + // should happen in most cases, then it tries to insert, and it should work + // unless the node has been deleted, and we just report the exception + Database.InsertOrUpdate(xmlItem); } - } - - //now insert the data, again if something fails here, the whole transaction is reversed - if (contentTypeIds == null) - { - var query = Query.Builder; - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); - } - else - { - foreach (var contentTypeId in contentTypeIds) + catch (Exception e) { - //copy local - var id = contentTypeId; - var query = Query.Builder.Where(x => x.ContentTypeId == id && x.Trashed == false); - RebuildXmlStructuresProcessQuery(serializer, query, tr, groupSize); + Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - - tr.Complete(); + baseId = xmlItems.Last().NodeId; } } - private void RebuildXmlStructuresProcessQuery(Func serializer, IQuery query, Transaction tr, int pageSize) - { - var pageIndex = 0; - var total = long.MinValue; - var processed = 0; - do - { - var descendants = GetPagedResultsByQuery(query, pageIndex, pageSize, out total, "Path", Direction.Ascending, true); - - var xmlItems = (from descendant in descendants - let xml = serializer(descendant) - select new ContentXmlDto { NodeId = descendant.Id, Xml = xml.ToDataString() }).ToArray(); - - //bulk insert it into the database - Database.BulkInsertRecords(xmlItems, tr); - - processed += xmlItems.Length; - - pageIndex++; - } while (processed < total); - } - public override IMember GetByVersion(Guid versionId) { var sql = GetBaseQuery(false); @@ -655,7 +619,7 @@ public IEnumerable GetPagedResultsByQuery(IQuery query, long p return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - ProcessQuery, orderBy, orderDirection, orderBySystemField, + sql => ProcessQuery(sql), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -695,59 +659,65 @@ protected override string GetEntityPropertyNameForOrderBy(string orderBy) return base.GetEntityPropertyNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql) + private IEnumerable ProcessQuery(Sql sql, bool withCache = false) { - //NOTE: This doesn't allow properties to be part of the query + // fetch returns a list so it's ok to iterate it in this method var dtos = Database.Fetch(sql); - var ids = dtos.Select(x => x.ContentVersionDto.ContentDto.ContentTypeId).ToArray(); + var content = new IMember[dtos.Count]; + var defs = new List(); - //content types - var contentTypes = ids.Length == 0 ? Enumerable.Empty() : _memberTypeRepository.GetAll(ids).ToArray(); + for (var i = 0; i < dtos.Count; i++) + { + var dto = dtos[i]; - var dtosWithContentTypes = dtos - //This select into and null check are required because we don't have a foreign damn key on the contentType column - // http://issues.umbraco.org/issue/U4-5503 - .Select(x => new { dto = x, contentType = contentTypes.FirstOrDefault(ct => ct.Id == x.ContentVersionDto.ContentDto.ContentTypeId) }) - .Where(x => x.contentType != null) - .ToArray(); + // if the cache contains the item, use it + if (withCache) + { + var cached = RuntimeCache.GetCacheItem(GetCacheIdKey(dto.NodeId)); + if (cached != null) + { + content[i] = cached; + continue; + } + } - //Go get the property data for each document - IEnumerable docDefs = dtosWithContentTypes.Select(d => new DocumentDefinition( - d.dto.NodeId, - d.dto.ContentVersionDto.VersionId, - d.dto.ContentVersionDto.VersionDate, - d.dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, - d.contentType)); + // else, need to fetch from the database + // content type repository is full-cache so OK to get each one independently + var contentType = _memberTypeRepository.Get(dto.ContentVersionDto.ContentDto.ContentTypeId); + var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.NodeId); + content[i] = factory.BuildEntity(dto); + + // need properties + defs.Add(new DocumentDefinition( + dto.NodeId, + dto.ContentVersionDto.VersionId, + dto.ContentVersionDto.VersionDate, + dto.ContentVersionDto.ContentDto.NodeDto.CreateDate, + contentType + )); + } - var propertyData = GetPropertyCollection(sql, docDefs); + // load all properties for all documents from database in 1 query + var propertyData = GetPropertyCollection(sql, defs); - return dtosWithContentTypes.Select(d => CreateMemberFromDto( - d.dto, - contentTypes.First(ct => ct.Id == d.dto.ContentVersionDto.ContentDto.ContentTypeId), - propertyData[d.dto.NodeId])); - } + // assign + var dtoIndex = 0; + foreach (var def in defs) + { + // move to corresponding item (which has to exist) + while (dtos[dtoIndex].NodeId != def.Id) dtoIndex++; - /// - /// Private method to create a member object from a MemberDto - /// - /// - /// - /// - /// - private IMember CreateMemberFromDto(MemberDto dto, - IMemberType contentType, - PropertyCollection propCollection) - { - var factory = new MemberFactory(contentType, NodeObjectTypeId, dto.ContentVersionDto.NodeId); - var member = factory.BuildEntity(dto); + // complete the item + var cc = content[dtoIndex]; + cc.Properties = propertyData[cc.Id]; - member.Properties = propCollection; + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((Entity)cc).ResetDirtyProperties(false); + } - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)member).ResetDirtyProperties(false); - return member; + return content; } /// diff --git a/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs b/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs index a15a7e15217f..9e6e3cf47ccc 100644 --- a/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/NotificationsRepository.cs @@ -18,6 +18,27 @@ public NotificationsRepository(IDatabaseUnitOfWork unitOfWork) _unitOfWork = unitOfWork; } + public IEnumerable GetUsersNotifications(IEnumerable userIds, string action, IEnumerable nodeIds, Guid objectType) + { + var nodeIdsA = nodeIds.ToArray(); + var syntax = ApplicationContext.Current.DatabaseContext.SqlSyntax; // bah + var sql = new Sql() + .Select("DISTINCT umbracoNode.id nodeId, umbracoUser.id userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action") + .From(syntax) + .InnerJoin(syntax).On(syntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(syntax).On(syntax, left => left.UserId, right => right.Id) + .Where(x => x.NodeObjectType == objectType) + .Where(x => x.Disabled == false) // only approved users + .Where(x => x.Action == action); // on the specified action + if (nodeIdsA.Length > 0) + sql + .WhereIn(x => x.NodeId, nodeIdsA); // for the specified nodes + sql + .OrderBy(x => x.Id, syntax) + .OrderBy(dto => dto.NodeId, syntax); + return _unitOfWork.Database.Fetch(sql).Select(x => new Notification(x.nodeId, x.userId, x.action, objectType)); + } + public IEnumerable GetUserNotifications(IUser user) { var sql = new Sql() diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs index be0808bb1903..4511ebe35d19 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationRepository.cs @@ -42,34 +42,17 @@ protected override IRelation PerformGet(int id) throw new Exception(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); var factory = new RelationFactory(relationType); - var entity = factory.BuildEntity(dto); - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); - - return entity; + return DtoToEntity(dto, factory); } - //TODO: Fix N+1 ! - protected override IEnumerable PerformGetAll(params int[] ids) { - if (ids.Any()) - { - foreach (var id in ids) - { - yield return Get(id); - } - } - else - { - var dtos = Database.Fetch("WHERE id > 0"); - foreach (var dto in dtos) - { - yield return Get(dto.Id); - } - } + var sql = GetBaseQuery(false); + if (ids.Length > 0) + sql.WhereIn(x => x.Id, ids); + sql.OrderBy(x => x.RelationType); + var dtos = Database.Fetch(sql); + return DtosToEntities(dtos); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -77,13 +60,36 @@ protected override IEnumerable PerformGetByQuery(IQuery qu var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); - + sql.OrderBy(x => x.RelationType); var dtos = Database.Fetch(sql); + return DtosToEntities(dtos); + } + + private IEnumerable DtosToEntities(IEnumerable dtos) + { + // in most cases, the relation type will be the same for all of them, + // plus we've ordered the relations by type, so try to allocate as few + // factories as possible - bearing in mind that relation types are cached + RelationFactory factory = null; + var relationTypeId = -1; - foreach (var dto in dtos) + return dtos.Select(x => { - yield return Get(dto.Id); - } + if (relationTypeId != x.RelationType) + factory = new RelationFactory(_relationTypeRepository.Get(relationTypeId = x.RelationType)); + return DtoToEntity(x, factory); + }); + } + + private static IRelation DtoToEntity(RelationDto dto, RelationFactory factory) + { + var entity = factory.BuildEntity(dto); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); + + return entity; } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs index df0ae0b224ae..c0a110feca97 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RelationTypeRepository.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Cache; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -18,50 +19,42 @@ namespace Umbraco.Core.Persistence.Repositories /// internal class RelationTypeRepository : PetaPocoRepositoryBase, IRelationTypeRepository { - public RelationTypeRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) + { } + + // assuming we don't have tons of relation types, use a FullDataSet policy, ie + // cache the entire GetAll result once in a single collection - which can expire + private FullDataSetRepositoryCachePolicyFactory _cachePolicyFactory; + protected override IRepositoryCachePolicyFactory CachePolicyFactory { + get + { + return _cachePolicyFactory + ?? (_cachePolicyFactory = new FullDataSetRepositoryCachePolicyFactory( + RuntimeCache, GetEntityId, () => PerformGetAll(), expires: true)); + } } #region Overrides of RepositoryBase protected override IRelationType PerformGet(int id) { - var sql = GetBaseQuery(false); - sql.Where(GetBaseWhereClause(), new { Id = id }); - - var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); - if (dto == null) - return null; - - var factory = new RelationTypeFactory(); - var entity = factory.BuildEntity(dto); - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((TracksChangesEntityBase)entity).ResetDirtyProperties(false); - - return entity; + // use the underlying GetAll which will force cache all content types + return GetAll().FirstOrDefault(x => x.Id == id); } protected override IEnumerable PerformGetAll(params int[] ids) { + var sql = GetBaseQuery(false); + + // should not happen due to the cache policy if (ids.Any()) - { - foreach (var id in ids) - { - yield return Get(id); - } - } - else - { - var dtos = Database.Fetch("WHERE id > 0"); - foreach (var dto in dtos) - { - yield return Get(dto.Id); - } - } + throw new NotImplementedException(); + + var dtos = Database.Fetch(sql); + var factory = new RelationTypeFactory(); + return dtos.Select(x => DtoToEntity(x, factory)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -71,11 +64,19 @@ protected override IEnumerable PerformGetByQuery(IQuery(sql); + var factory = new RelationTypeFactory(); + return dtos.Select(x => DtoToEntity(x, factory)); + } - foreach (var dto in dtos) - { - yield return Get(dto.Id); - } + private static IRelationType DtoToEntity(RelationTypeDto dto, RelationTypeFactory factory) + { + var entity = factory.BuildEntity(dto); + + //on initial construction we don't want to have dirty properties tracked + // http://issues.umbraco.org/issue/U4-1946 + ((TracksChangesEntityBase) entity).ResetDirtyProperties(false); + + return entity; } #endregion diff --git a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs index 5534a9ea40bb..41946d48d43a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RepositoryBase.cs @@ -83,6 +83,12 @@ protected RepositoryBase(IUnitOfWork work, CacheHelper cache, ILogger logger) } + #region Static Queries + + private IQuery _hasIdQuery; + + #endregion + protected virtual TId GetEntityId(TEntity entity) { return (TId)(object)entity.Id; @@ -111,9 +117,14 @@ protected virtual IRepositoryCachePolicyFactory CachePolicyFactory RuntimeCache, new RepositoryCachePolicyOptions(() => { + //create it once if it is needed (no need for locking here) + if (_hasIdQuery == null) + { + _hasIdQuery = Query.Builder.Where(x => x.Id != 0); + } + //Get count of all entities of current type (TEntity) to ensure cached result is correct - var query = Query.Builder.Where(x => x.Id != 0); - return PerformCount(query); + return PerformCount(_hasIdQuery); }))); } } diff --git a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs index 68f5496101ed..142740e9d839 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TagRepository.cs @@ -565,7 +565,7 @@ private string GetTagSet(IEnumerable tagsToInsert) var array = tagsToInsert .Select(tag => - string.Format("select '{0}' as Tag, '{1}' as " + SqlSyntax.GetQuotedColumnName("group") + @"", + string.Format("select N'{0}' as Tag, '{1}' as " + SqlSyntax.GetQuotedColumnName("group") + @"", PetaPocoExtensions.EscapeAtSymbols(tag.Text.Replace("'", "''")), tag.Group)) .ToArray(); return "(" + string.Join(" union ", array).Replace(" ", " ") + ") as TagSet"; diff --git a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs index 6bd83a8ad8c7..4652ce47be1b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/UserRepository.cs @@ -320,51 +320,51 @@ public IEnumerable GetUsersAssignedToSection(string sectionAlias) /// public IEnumerable GetPagedResultsByQuery(IQuery query, int pageIndex, int pageSize, out int totalRecords, Expression> orderBy) { - if (orderBy == null) throw new ArgumentNullException("orderBy"); + if (orderBy == null) + throw new ArgumentNullException("orderBy"); - var sql = new Sql(); - sql.Select("*").From(); - - Sql resultQuery; - if (query != null) - { - var translator = new SqlTranslator(sql, query); - resultQuery = translator.Translate(); - } - else - { - resultQuery = sql; - } - - //get the referenced column name + // get the referenced column name and find the corresp mapped column name var expressionMember = ExpressionHelper.GetMemberInfo(orderBy); - //now find the mapped column name var mapper = MappingResolver.Current.ResolveMapperByType(typeof(IUser)); var mappedField = mapper.Map(expressionMember.Name); + if (mappedField.IsNullOrWhiteSpace()) - { throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause"); - } - //need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 - resultQuery.OrderBy(string.Format("({0})", mappedField)); - var pagedResult = Database.Page(pageIndex + 1, pageSize, resultQuery); + var sql = new Sql() + .Select("umbracoUser.Id") + .From(SqlSyntax); + + var idsQuery = query == null ? sql : new SqlTranslator(sql, query).Translate(); - totalRecords = Convert.ToInt32(pagedResult.TotalItems); + // need to ensure the order by is in brackets, see: https://github.com/toptensoftware/PetaPoco/issues/177 + idsQuery.OrderBy("(" + mappedField + ")"); + var page = Database.Page(pageIndex + 1, pageSize, idsQuery); + totalRecords = Convert.ToInt32(page.TotalItems); - //now that we have the user dto's we need to construct true members from the list. if (totalRecords == 0) - { return Enumerable.Empty(); - } - var ids = pagedResult.Items.Select(x => x.Id).ToArray(); - var result = ids.Length == 0 ? Enumerable.Empty() : GetAll(ids); + // now get the actual users and ensure they are ordered properly (same clause) + var ids = page.Items.ToArray(); + return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(orderBy.Compile()); + } + + internal IEnumerable GetNextUsers(int id, int count) + { + var idsQuery = new Sql() + .Select("umbracoUser.Id") + .From(SqlSyntax) + .Where(x => x.Id >= id) + .OrderBy(x => x.Id, SqlSyntax); + + // first page is index 1, not zero + var ids = Database.Page(1, count, idsQuery).Items.ToArray(); - //now we need to ensure this result is also ordered by the same order by clause - return result.OrderBy(orderBy.Compile()); + // now get the actual users and ensure they are ordered properly (same clause) + return ids.Length == 0 ? Enumerable.Empty() : GetAll(ids).OrderBy(x => x.Id); } - + /// /// Returns permissions for a given user for any number of nodes /// diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 4ef493a22a23..ec1623d16a2c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -40,6 +40,11 @@ protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, #region IRepositoryVersionable Implementation + /// + /// Gets a list of all versions for an ordered so latest is first + /// + /// Id of the to retrieve versions from + /// An enumerable list of the same object with different versions public virtual IEnumerable GetAllVersions(int id) { var sql = new Sql(); @@ -60,6 +65,28 @@ public virtual IEnumerable GetAllVersions(int id) } } + /// + /// Gets a list of all version Ids for the given content item ordered so latest is first + /// + /// + /// The maximum number of rows to return + /// + public virtual IEnumerable GetVersionIds(int id, int maxRows) + { + var sql = new Sql(); + sql.Select("cmsDocument.versionId") + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.NodeId, right => right.NodeId) + .Where(x => x.NodeObjectType == NodeObjectTypeId) + .Where(x => x.NodeId == id) + .OrderByDescending(x => x.UpdateDate, SqlSyntax); + + return Database.Fetch(SqlSyntax.SelectTop(sql, maxRows)); + } + public virtual void DeleteVersion(Guid versionId) { var dto = Database.FirstOrDefault("WHERE versionId = @VersionId", new { VersionId = versionId }); @@ -583,6 +610,8 @@ protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) return "cmsContentVersion.VersionDate"; case "NAME": return "umbracoNode.text"; + case "PUBLISHED": + return "cmsDocument.published"; case "OWNER": //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter return "umbracoNode.nodeUser"; diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 85eb00ea879d..ff0f9e90287e 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -197,7 +197,7 @@ public virtual IRelationRepository CreateRelationRepository(IDatabaseUnitOfWork { return new RelationRepository( uow, - _noCache, //never cache + _noCache, _logger, _sqlSyntax, CreateRelationTypeRepository(uow)); } @@ -206,7 +206,7 @@ public virtual IRelationTypeRepository CreateRelationTypeRepository(IDatabaseUni { return new RelationTypeRepository( uow, - _noCache, //never cache + _cacheHelper, _logger, _sqlSyntax); } @@ -343,4 +343,4 @@ public IRedirectUrlRepository CreateRedirectUrlRepository(IDatabaseUnitOfWork uo _sqlSyntax); } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs b/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs index 507db230cc0e..c9ef4d1f35b5 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/DbTypes.cs @@ -4,6 +4,7 @@ namespace Umbraco.Core.Persistence.SqlSyntax { + //TODO: TSyntax should be removed, it's not used/needed here public class DbTypes where TSyntax : ISqlSyntaxProvider { diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs index 449f5fb3b17e..1ea600b6e4eb 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/MicrosoftSqlSyntaxProviderBase.cs @@ -1,4 +1,6 @@ using System; +using System.Data; +using System.Linq; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.SqlSyntax @@ -133,5 +135,111 @@ public override string GetStringColumnWildcardComparison(string column, string v throw new ArgumentOutOfRangeException("columnType"); } } + + /// + /// This uses a the DbTypeMap created and custom mapping to resolve the SqlDbType + /// + /// + /// + public virtual SqlDbType GetSqlDbType(Type clrType) + { + var dbType = DbTypeMap.ColumnDbTypeMap.First(x => x.Key == clrType).Value; + return GetSqlDbType(dbType); + } + + /// + /// Returns the mapped SqlDbType for the DbType specified + /// + /// + /// + public virtual SqlDbType GetSqlDbType(DbType dbType) + { + var sqlDbType = SqlDbType.NVarChar; + + //SEE: https://msdn.microsoft.com/en-us/library/cc716729(v=vs.110).aspx + // and https://msdn.microsoft.com/en-us/library/yy6y35y8%28v=vs.110%29.aspx?f=255&MSPPError=-2147217396 + switch (dbType) + { + case DbType.AnsiString: + sqlDbType = SqlDbType.VarChar; + break; + case DbType.Binary: + sqlDbType = SqlDbType.VarBinary; + break; + case DbType.Byte: + sqlDbType = SqlDbType.TinyInt; + break; + case DbType.Boolean: + sqlDbType = SqlDbType.Bit; + break; + case DbType.Currency: + sqlDbType = SqlDbType.Money; + break; + case DbType.Date: + sqlDbType = SqlDbType.Date; + break; + case DbType.DateTime: + sqlDbType = SqlDbType.DateTime; + break; + case DbType.Decimal: + sqlDbType = SqlDbType.Decimal; + break; + case DbType.Double: + sqlDbType = SqlDbType.Float; + break; + case DbType.Guid: + sqlDbType = SqlDbType.UniqueIdentifier; + break; + case DbType.Int16: + sqlDbType = SqlDbType.SmallInt; + break; + case DbType.Int32: + sqlDbType = SqlDbType.Int; + break; + case DbType.Int64: + sqlDbType = SqlDbType.BigInt; + break; + case DbType.Object: + sqlDbType = SqlDbType.Variant; + break; + case DbType.SByte: + throw new NotSupportedException("Inferring a SqlDbType from SByte is not supported."); + case DbType.Single: + sqlDbType = SqlDbType.Real; + break; + case DbType.String: + sqlDbType = SqlDbType.NVarChar; + break; + case DbType.Time: + sqlDbType = SqlDbType.Time; + break; + case DbType.UInt16: + throw new NotSupportedException("Inferring a SqlDbType from UInt16 is not supported."); + case DbType.UInt32: + throw new NotSupportedException("Inferring a SqlDbType from UInt32 is not supported."); + case DbType.UInt64: + throw new NotSupportedException("Inferring a SqlDbType from UInt64 is not supported."); + case DbType.VarNumeric: + throw new NotSupportedException("Inferring a VarNumeric from UInt64 is not supported."); + case DbType.AnsiStringFixedLength: + sqlDbType = SqlDbType.Char; + break; + case DbType.StringFixedLength: + sqlDbType = SqlDbType.NChar; + break; + case DbType.Xml: + sqlDbType = SqlDbType.Xml; + break; + case DbType.DateTime2: + sqlDbType = SqlDbType.DateTime2; + break; + case DbType.DateTimeOffset: + sqlDbType = SqlDbType.DateTimeOffset; + break; + default: + throw new ArgumentOutOfRangeException(); + } + return sqlDbType; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs index 4c35c90e1bfb..6cc97bdfb369 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs @@ -6,16 +6,11 @@ namespace Umbraco.Core.Persistence.SqlSyntax { /// - /// Represents an SqlSyntaxProvider for Sql Server + /// Represents an SqlSyntaxProvider for Sql Server. /// - [SqlSyntaxProviderAttribute(Constants.DatabaseProviders.SqlServer)] + [SqlSyntaxProvider(Constants.DatabaseProviders.SqlServer)] public class SqlServerSyntaxProvider : MicrosoftSqlSyntaxProviderBase { - public SqlServerSyntaxProvider() - { - - } - /// /// Gets/sets the version of the current SQL server instance /// @@ -31,7 +26,7 @@ internal SqlServerVersionName GetVersionName(Database database) switch (firstPart) { case "13": - _versionName = SqlServerVersionName.V2014; + _versionName = SqlServerVersionName.V2016; break; case "12": _versionName = SqlServerVersionName.V2014; @@ -75,7 +70,7 @@ public IEnumerable> GetDefaultConstraintsP { var items = db.Fetch("SELECT TableName = t.Name,ColumnName = c.Name,dc.Name,dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id"); return items.Select(x => new Tuple(x.TableName, x.ColumnName, x.Name, x.Definition)); - } + } public override IEnumerable GetTablesInSchema(Database db) { @@ -120,9 +115,9 @@ public override IEnumerable> GetDefinedIndex inner join sys.all_columns as AC on IC.[object_id] = AC.[object_id] and IC.[column_id] = AC.[column_id] WHERE I.name NOT LIKE 'PK_%' order by T.name, I.name"); - return items.Select(item => new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, + return items.Select(item => new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME, item.UNIQUE == 1)).ToList(); - + } public override bool DoesTableExist(Database db, string tableName) @@ -164,7 +159,7 @@ protected override string FormatSystemMethods(SystemMethods systemMethod) switch (systemMethod) { case SystemMethods.NewGuid: - return "NEWID()"; + return "NEWID()"; case SystemMethods.CurrentDateTime: return "GETDATE()"; //case SystemMethods.NewSequentialId: @@ -181,11 +176,11 @@ public override string DeleteDefaultConstraint get { return "ALTER TABLE [{0}] DROP CONSTRAINT [DF_{0}_{1}]"; } } - + public override string DropIndex { get { return "DROP INDEX {0} ON {1}"; } } public override string RenameColumn { get { return "sp_rename '{0}.{1}', '{2}', 'COLUMN'"; } } - + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Properties/AssemblyInfo.cs b/src/Umbraco.Core/Properties/AssemblyInfo.cs index d1ddadde377b..dbc1ab6c93da 100644 --- a/src/Umbraco.Core/Properties/AssemblyInfo.cs +++ b/src/Umbraco.Core/Properties/AssemblyInfo.cs @@ -32,6 +32,7 @@ [assembly: InternalsVisibleTo("umbraco.editorControls")] [assembly: InternalsVisibleTo("Umbraco.Tests")] +[assembly: InternalsVisibleTo("Umbraco.Tests.Benchmarks")] [assembly: InternalsVisibleTo("Umbraco.Core")] [assembly: InternalsVisibleTo("Umbraco.Web")] [assembly: InternalsVisibleTo("Umbraco.Web.UI")] diff --git a/src/Umbraco.Core/Security/AuthenticationExtensions.cs b/src/Umbraco.Core/Security/AuthenticationExtensions.cs index 9007c459464f..3c88c07edf4f 100644 --- a/src/Umbraco.Core/Security/AuthenticationExtensions.cs +++ b/src/Umbraco.Core/Security/AuthenticationExtensions.cs @@ -113,8 +113,8 @@ public static UmbracoBackOfficeIdentity GetCurrentIdentity(this HttpContextBase //Otherwise convert to a UmbracoBackOfficeIdentity if it's auth'd and has the back office session var claimsIdentity = http.User.Identity as ClaimsIdentity; - if (claimsIdentity != null && claimsIdentity.IsAuthenticated) - { + if (claimsIdentity != null && claimsIdentity.IsAuthenticated && claimsIdentity.HasClaim(x => x.Type == Constants.Security.SessionIdClaimType)) + { try { return UmbracoBackOfficeIdentity.FromClaimsIdentity(claimsIdentity); diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index c6714e256a95..08c6f54dcf84 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -644,6 +644,13 @@ private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIden { anythingChanged = true; user.IsLockedOut = identityUser.IsLockedOut; + + if (user.IsLockedOut) + { + //need to set the last lockout date + user.LastLockoutDate = DateTime.Now; + } + } if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index cbcd5ecc05b3..6223e5933b28 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -56,6 +56,12 @@ public ContentService( _userService = userService; } + #region Static Queries + + private readonly IQuery _notTrashedQuery = Query.Builder.Where(x => x.Trashed == false); + + #endregion + public int CountPublished(string contentTypeAlias = null) { var uow = UowProvider.GetUnitOfWork(); @@ -342,11 +348,23 @@ public IContent GetById(int id) /// public IEnumerable GetByIds(IEnumerable ids) { - if (ids.Any() == false) return Enumerable.Empty(); + var idsArray = ids.ToArray(); + if (idsArray.Length == 0) return Enumerable.Empty(); using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { - return repository.GetAll(ids.ToArray()); + //ensure that the result has the order based on the ids passed in + var result = repository.GetAll(idsArray); + + var content = result.ToDictionary(x => x.Id, x => x); + + var sortedResult = idsArray.Select(x => + { + IContent c; + return content.TryGetValue(x, out c) ? c : null; + }).WhereNotNull(); + + return sortedResult; } } @@ -436,6 +454,21 @@ public IEnumerable GetVersions(int id) } } + /// + /// Gets a list of all version Ids for the given content item ordered so latest is first + /// + /// + /// The maximum number of rows to return + /// + public IEnumerable GetVersionIds(int id, int maxRows) + { + using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) + { + var versions = repository.GetVersionIds(id, maxRows); + return versions; + } + } + /// /// Gets a collection of objects, which are ancestors of the current content. /// @@ -762,8 +795,7 @@ internal IEnumerable GetAllPublished() { using (var repository = RepositoryFactory.CreateContentRepository(UowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.Trashed == false); - return repository.GetByPublishedVersion(query); + return repository.GetByPublishedVersion(_notTrashedQuery); } } diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index d80a93a4abe2..31e86cd12840 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -141,7 +141,7 @@ private Attempt SaveContainer( { var evtMsgs = EventMessagesFactory.Get(); - if (container.ContainedObjectType != containerObjectType) + if (container.ContainerObjectType != containerObjectType) { var ex = new InvalidOperationException("Not a " + objectTypeName + " container."); return OperationStatus.Exception(evtMsgs, ex); @@ -799,7 +799,7 @@ public void Delete(IContentType contentType, int userId = 0) // of a different type, move them to the recycle bin, then permanently delete the content items. // The main problem with this is that for every content item being deleted, events are raised... // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. - + var deletedContentTypes = new List() {contentType}; deletedContentTypes.AddRange(contentType.Descendants().OfType()); @@ -807,7 +807,7 @@ public void Delete(IContentType contentType, int userId = 0) { _contentService.DeleteContentOfType(deletedContentType.Id); } - + repository.Delete(contentType); uow.Commit(); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 257416d05493..a6b84704fc77 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -62,6 +62,12 @@ public EntityService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory rep } + #region Static Queries + + private readonly IQuery _rootEntityQuery = Query.Builder.Where(x => x.ParentId == -1); + + #endregion + /// /// Returns the integer id for a given GUID /// @@ -389,8 +395,7 @@ public virtual IEnumerable GetRootEntities(UmbracoObjectTypes um var objectTypeId = umbracoObjectType.GetGuid(); using (var repository = RepositoryFactory.CreateEntityRepository(UowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.ParentId == -1); - var entities = repository.GetByQuery(query, objectTypeId); + var entities = repository.GetByQuery(_rootEntityQuery, objectTypeId); return entities; } diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index fa4130f8c42d..ec5db8c4fb91 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -298,6 +298,14 @@ IEnumerable GetPagedDescendants(int id, long pageIndex, int pageSize, /// An Enumerable list of objects IEnumerable GetVersions(int id); + /// + /// Gets a list of all version Ids for the given content item ordered so latest is first + /// + /// + /// The maximum number of rows to return + /// + IEnumerable GetVersionIds(int id, int maxRows); + /// /// Gets a collection of objects, which reside at the first level / root /// diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 6ff8f754025d..d25ddf7f58a1 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Xml.Linq; using Umbraco.Core.Configuration; using Umbraco.Core.Models; using Umbraco.Core.Persistence.DatabaseModelDefinitions; @@ -57,6 +58,19 @@ public interface IMediaServiceOperations /// public interface IMediaService : IService { + /// + /// Gets all XML entries found in the cmsContentXml table based on the given path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of media items + /// + /// If -1 is passed, then this will return all media xml entries, otherwise will return all descendents from the path + /// + IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords); + /// /// Rebuilds all xml content in the cmsContentXml table for all media /// diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 29235cc7ab34..a1182ce80adb 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -1199,6 +1199,27 @@ public bool Sort(IEnumerable items, int userId = 0, bool raiseEvents = t return true; } + /// + /// Gets paged media descendants as XML by path + /// + /// Path starts with + /// Page number + /// Page size + /// Total records the query would return without paging + /// A paged enumerable of XML entries of media items + public IEnumerable GetPagedXmlEntries(string path, long pageIndex, int pageSize, out long totalRecords) + { + Mandate.ParameterCondition(pageIndex >= 0, "pageIndex"); + Mandate.ParameterCondition(pageSize > 0, "pageSize"); + + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaRepository(uow)) + { + var contents = repository.GetPagedXmlEntriesByPath(path, pageIndex, pageSize, out totalRecords); + return contents; + } + } + /// /// Rebuilds all xml content in the cmsContentXml table for all media /// diff --git a/src/Umbraco.Core/Services/NotificationService.cs b/src/Umbraco.Core/Services/NotificationService.cs index 5d51e200082e..52b26ab1b32a 100644 --- a/src/Umbraco.Core/Services/NotificationService.cs +++ b/src/Umbraco.Core/Services/NotificationService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -15,7 +16,6 @@ using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Strings; -using umbraco.interfaces; namespace Umbraco.Core.Services { @@ -55,38 +55,72 @@ public void SendNotifications(IUser operatingUser, IUmbracoEntity entity, string Func createSubject, Func createBody) { - if ((entity is IContent) == false) + if (entity is IContent == false) throw new NotSupportedException(); - var content = (IContent) entity; - - // lazily get versions - into a list to ensure we can enumerate multiple times - List allVersions = null; - - int totalUsers; - var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers); - foreach (var u in allUsers.Where(x => x.IsApproved)) + var content = (IContent) entity; + + // lazily get previous version + IContentBase prevVersion = null; + + // do not load *all* users in memory at once + // do not load notifications *per user* (N+1 select) + // cannot load users & notifications in 1 query (combination btw User2AppDto and User2NodeNotifyDto) + // => get batches of users, get all their notifications in 1 query + // re. users: + // users being (dis)approved = not an issue, filtered in memory not in SQL + // users being modified or created = not an issue, ordering by ID, as long as we don't *insert* low IDs + // users being deleted = not an issue for GetNextUsers + var id = 0; + var nodeIds = content.Path.Split(',').Select(int.Parse).ToArray(); + const int pagesz = 400; // load batches of 400 users + do { - var userNotifications = GetUserNotifications(u, content.Path); - var notificationForAction = userNotifications.FirstOrDefault(x => x.Action == action); - if (notificationForAction == null) continue; - - if (allVersions == null) // lazy load - allVersions = _contentService.GetVersions(entity.Id).ToList(); + // users are returned ordered by id, notifications are returned ordered by user id + var users = ((UserService) _userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList(); + var notifications = GetUsersNotifications(users.Select(x => x.Id), action, nodeIds, Constants.ObjectTypes.DocumentGuid).ToList(); + if (notifications.Count == 0) break; - try + var i = 0; + foreach (var user in users) { - SendNotification(operatingUser, u, content, allVersions, - actionName, http, createSubject, createBody); + // continue if there's no notification for this user + if (notifications[i].UserId != user.Id) continue; // next user - _logger.Debug(string.Format("Notification type: {0} sent to {1} ({2})", - action, u.Name, u.Email)); - } - catch (Exception ex) - { - _logger.Error("An error occurred sending notification", ex); + // lazy load prev version + if (prevVersion == null) + { + prevVersion = GetPreviousVersion(entity.Id); + } + + // queue notification + var req = CreateNotificationRequest(operatingUser, user, content, prevVersion, actionName, http, createSubject, createBody); + Enqueue(req); + + // skip other notifications for this user + while (i < notifications.Count && notifications[i++].UserId == user.Id) ; + if (i >= notifications.Count) break; // break if no more notifications } - } + + // load more users if any + id = users.Count == pagesz ? users.Last().Id + 1 : -1; + + } while (id > 0); + } + + /// + /// Gets the previous version to the latest version of the content item if there is one + /// + /// + /// + private IContentBase GetPreviousVersion(int contentId) + { + // Regarding this: http://issues.umbraco.org/issue/U4-5180 + // we know they are descending from the service so we know that newest is first + // we are only selecting the top 2 rows since that is all we need + var allVersions = _contentService.GetVersionIds(contentId, 2).ToList(); + var prevVersionIndex = allVersions.Count > 1 ? 1 : 0; + return _contentService.GetByVersion(allVersions[prevVersionIndex]); } /// @@ -106,47 +140,76 @@ public void SendNotifications(IUser operatingUser, IEnumerable e Func createSubject, Func createBody) { - if ((entities is IEnumerable) == false) + if (entities is IEnumerable == false) throw new NotSupportedException(); - // ensure we can enumerate multiple times var entitiesL = entities as List ?? entities.Cast().ToList(); - // lazily get versions - into lists to ensure we can enumerate multiple times - var allVersionsDictionary = new Dictionary>(); + //exit if there are no entities + if (entitiesL.Count == 0) return; - int totalUsers; - var allUsers = _userService.GetAll(0, int.MaxValue, out totalUsers); - foreach (var u in allUsers.Where(x => x.IsApproved)) - { - var userNotifications = GetUserNotifications(u).ToArray(); + //put all entity's paths into a list with the same indicies + var paths = entitiesL.Select(x => x.Path.Split(',').Select(int.Parse).ToArray()).ToArray(); - foreach (var content in entitiesL) - { - var userNotificationsByPath = FilterUserNotificationsByPath(userNotifications, content.Path); - var notificationForAction = userNotificationsByPath.FirstOrDefault(x => x.Action == action); - if (notificationForAction == null) continue; + // lazily get versions + var prevVersionDictionary = new Dictionary(); - var allVersions = allVersionsDictionary.ContainsKey(content.Id) // lazy load - ? allVersionsDictionary[content.Id] - : allVersionsDictionary[content.Id] = _contentService.GetVersions(content.Id).ToList(); + // see notes above + var id = 0; + const int pagesz = 400; // load batches of 400 users + do + { + // users are returned ordered by id, notifications are returned ordered by user id + var users = ((UserService)_userService).GetNextUsers(id, pagesz).Where(x => x.IsApproved).ToList(); + var notifications = GetUsersNotifications(users.Select(x => x.Id), action, Enumerable.Empty(), Constants.ObjectTypes.DocumentGuid).ToList(); + if (notifications.Count == 0) break; + + var i = 0; + foreach (var user in users) + { + // continue if there's no notification for this user + if (notifications[i].UserId != user.Id) continue; // next user - try + for (var j = 0; j < entitiesL.Count; j++) { - SendNotification(operatingUser, u, content, allVersions, - actionName, http, createSubject, createBody); - - _logger.Debug(string.Format("Notification type: {0} sent to {1} ({2})", - action, u.Name, u.Email)); + var content = entitiesL[j]; + var path = paths[j]; + + // test if the notification applies to the path ie to this entity + if (path.Contains(notifications[i].EntityId) == false) continue; // next entity + + if (prevVersionDictionary.ContainsKey(content.Id) == false) + { + prevVersionDictionary[content.Id] = GetPreviousVersion(content.Id); + } + + // queue notification + var req = CreateNotificationRequest(operatingUser, user, content, prevVersionDictionary[content.Id], actionName, http, createSubject, createBody); + Enqueue(req); } - catch (Exception ex) + + // skip other notifications for this user, essentially this means moving i to the next index of notifications + // for the next user. + do { - _logger.Error("An error occurred sending notification", ex); - } - } - } + i++; + } while (i < notifications.Count && notifications[i].UserId == user.Id); + + if (i >= notifications.Count) break; // break if no more notifications + } + + // load more users if any + id = users.Count == pagesz ? users.Last().Id + 1 : -1; + + } while (id > 0); } + private IEnumerable GetUsersNotifications(IEnumerable userIds, string action, IEnumerable nodeIds, Guid objectType) + { + var uow = _uowProvider.GetUnitOfWork(); + var repository = new NotificationsRepository(uow); + return repository.GetUsersNotifications(userIds, action, nodeIds, objectType); + } /// /// Gets the notifications for the user @@ -184,7 +247,7 @@ public IEnumerable GetUserNotifications(IUser user, string path) public IEnumerable FilterUserNotificationsByPath(IEnumerable userNotifications, string path) { var pathParts = path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); + return userNotifications.Where(r => pathParts.InvariantContains(r.EntityId.ToString(CultureInfo.InvariantCulture))).ToList(); } /// @@ -254,29 +317,23 @@ public Notification CreateNotification(IUser user, IEntity entity, string action /// /// /// - /// + /// /// The action readable name - currently an action is just a single letter, this is the name associated with the letter /// /// Callback to create the mail subject /// Callback to create the mail body - private void SendNotification(IUser performingUser, IUser mailingUser, IContent content, IEnumerable allVersions, string actionName, HttpContextBase http, + private NotificationRequest CreateNotificationRequest(IUser performingUser, IUser mailingUser, IContentBase content, IContentBase oldDoc, + string actionName, HttpContextBase http, Func createSubject, Func createBody) { if (performingUser == null) throw new ArgumentNullException("performingUser"); if (mailingUser == null) throw new ArgumentNullException("mailingUser"); if (content == null) throw new ArgumentNullException("content"); - if (allVersions == null) throw new ArgumentNullException("allVersions"); if (http == null) throw new ArgumentNullException("http"); if (createSubject == null) throw new ArgumentNullException("createSubject"); - if (createBody == null) throw new ArgumentNullException("createBody"); - - //Ensure they are sorted: http://issues.umbraco.org/issue/U4-5180 - var allVersionsAsArray = allVersions.OrderBy(x => x.UpdateDate).ToArray(); - - int versionCount = (allVersionsAsArray.Length > 1) ? (allVersionsAsArray.Length - 2) : (allVersionsAsArray.Length - 1); - var oldDoc = _contentService.GetByVersion(allVersionsAsArray[versionCount].Version); - + if (createBody == null) throw new ArgumentNullException("createBody"); + // build summary var summary = new StringBuilder(); var props = content.Properties.ToArray(); @@ -290,16 +347,16 @@ private void SendNotification(IUser performingUser, IUser mailingUser, IContent { var oldProperty = oldDoc.Properties[p.PropertyType.Alias]; oldText = oldProperty.Value != null ? oldProperty.Value.ToString() : ""; - + // replace html with char equivalent ReplaceHtmlSymbols(ref oldText); ReplaceHtmlSymbols(ref newText); } - + // make sure to only highlight changes done using TinyMCE editor... other changes will be displayed using default summary // TODO: We should probably allow more than just tinymce?? - if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias) + if ((p.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TinyMCEAlias) && string.CompareOrdinal(oldText, newText) != 0) { summary.Append(""); @@ -308,26 +365,31 @@ private void SendNotification(IUser performingUser, IUser mailingUser, IContent " Red for deleted characters Yellow for inserted characters"); summary.Append(""); summary.Append(""); - summary.Append(" New " + - p.PropertyType.Name + ""); - summary.Append("" + - ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request) + - ""); + summary.Append(" New "); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(ReplaceLinks(CompareText(oldText, newText, true, false, "", string.Empty), http.Request)); + summary.Append(""); summary.Append(""); summary.Append(""); - summary.Append(" Old " + - p.PropertyType.Name + ""); - summary.Append("" + - ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request) + - ""); + summary.Append(" Old "); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(ReplaceLinks(CompareText(newText, oldText, true, false, "", string.Empty), http.Request)); + summary.Append(""); summary.Append(""); } else { summary.Append(""); - summary.Append("" + - p.PropertyType.Name + ""); - summary.Append("" + newText + ""); + summary.Append(""); + summary.Append(p.PropertyType.Name); + summary.Append(""); + summary.Append(""); + summary.Append(newText); + summary.Append(""); summary.Append(""); } summary.Append( @@ -338,29 +400,27 @@ private void SendNotification(IUser performingUser, IUser mailingUser, IContent string[] subjectVars = { - http.Request.ServerVariables["SERVER_NAME"] + ":" + - http.Request.Url.Port + - IOHelper.ResolveUrl(SystemDirectories.Umbraco), + string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), actionName, content.Name }; string[] bodyVars = { - mailingUser.Name, - actionName, - content.Name, + mailingUser.Name, + actionName, + content.Name, performingUser.Name, - http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port + IOHelper.ResolveUrl(SystemDirectories.Umbraco), + string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port, IOHelper.ResolveUrl(SystemDirectories.Umbraco)), content.Id.ToString(CultureInfo.InvariantCulture), summary.ToString(), string.Format("{2}://{0}/{1}", - http.Request.ServerVariables["SERVER_NAME"] + ":" + http.Request.Url.Port, + string.Concat(http.Request.ServerVariables["SERVER_NAME"], ":", http.Request.Url.Port), //TODO: RE-enable this so we can have a nice url /*umbraco.library.NiceUrl(documentObject.Id))*/ - content.Id + ".aspx", + string.Concat(content.Id, ".aspx"), protocol) - + }; - // create the mail message + // create the mail message var mail = new MailMessage(UmbracoConfig.For.UmbracoSettings().Content.NotificationEmailAddress, mailingUser.Email); // populate the message @@ -374,10 +434,10 @@ private void SendNotification(IUser performingUser, IUser mailingUser, IContent { mail.IsBodyHtml = true; mail.Body = - @" + string.Concat(@" -" + createBody(mailingUser, bodyVars); +", createBody(mailingUser, bodyVars)); } // nh, issue 30724. Due to hardcoded http strings in resource files, we need to check for https replacements here @@ -390,32 +450,17 @@ private void SendNotification(IUser performingUser, IUser mailingUser, IContent string.Format("https://{0}", serverName)); } - - // send it asynchronously, we don't want to got up all of the request time to send emails! - ThreadPool.QueueUserWorkItem(state => - { - try - { - using (mail) - { - using (var sender = new SmtpClient()) - { - sender.Send(mail); - } - } - - } - catch (Exception ex) - { - _logger.Error("An error occurred sending notification", ex); - } - }); + return new NotificationRequest(mail, actionName, mailingUser.Name, mailingUser.Email); } private static string ReplaceLinks(string text, HttpRequestBase request) { - string domain = GlobalSettings.UseSSL ? "https://" : "http://"; - domain += request.ServerVariables["SERVER_NAME"] + ":" + request.Url.Port + "/"; + var sb = new StringBuilder(GlobalSettings.UseSSL ? "https://" : "http://"); + sb.Append(request.ServerVariables["SERVER_NAME"]); + sb.Append(":"); + sb.Append(request.Url.Port); + sb.Append("/"); + var domain = sb.ToString(); text = text.Replace("href=\"/", "href=\"" + domain); text = text.Replace("src=\"/", "src=\"" + domain); return text; @@ -484,7 +529,7 @@ private static string CompareText(string oldText, string newText, bool displayIn pos++; } // while sb.Append(""); - } // if + } // if } // while // write rest of unchanged chars @@ -495,8 +540,95 @@ private static string CompareText(string oldText, string newText, bool displayIn } // while return sb.ToString(); + } + + // manage notifications + // ideally, would need to use IBackgroundTasks - but they are not part of Core! + + private static readonly object Locker = new object(); + private static readonly BlockingCollection Queue = new BlockingCollection(); + private static volatile bool _running; + + private void Enqueue(NotificationRequest notification) + { + Queue.Add(notification); + if (_running) return; + lock (Locker) + { + if (_running) return; + Process(Queue); + _running = true; + } + } + + private class NotificationRequest + { + public NotificationRequest(MailMessage mail, string action, string userName, string email) + { + Mail = mail; + Action = action; + UserName = userName; + Email = email; + } + + public MailMessage Mail { get; private set; } + + public string Action { get; private set; } + + public string UserName { get; private set; } + + public string Email { get; private set; } } + private void Process(BlockingCollection notificationRequests) + { + ThreadPool.QueueUserWorkItem(state => + { + var s = new SmtpClient(); + try + { + _logger.Debug("Begin processing notifications."); + while (true) + { + NotificationRequest request; + while (notificationRequests.TryTake(out request, 8 * 1000)) // stay on for 8s + { + try + { + if (Sendmail != null) Sendmail(s, request.Mail, _logger); else s.Send(request.Mail); + _logger.Debug(string.Format("Notification \"{0}\" sent to {1} ({2})", request.Action, request.UserName, request.Email)); + } + catch (Exception ex) + { + _logger.Error("An error occurred sending notification", ex); + s.Dispose(); + s = new SmtpClient(); + } + finally + { + request.Mail.Dispose(); + } + } + lock (Locker) + { + if (notificationRequests.Count > 0) continue; // last chance + _running = false; // going down + break; + } + } + } + finally + { + s.Dispose(); + } + _logger.Debug("Done processing notifications."); + }); + } + + // for tests + internal static Action Sendmail; + //= (_, msg, logger) => logger.Debug("Email " + msg.To.ToString()); + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/UserService.cs b/src/Umbraco.Core/Services/UserService.cs index c7a63a884beb..8e984d1e5d5d 100644 --- a/src/Umbraco.Core/Services/UserService.cs +++ b/src/Umbraco.Core/Services/UserService.cs @@ -9,6 +9,7 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Querying; +using Umbraco.Core.Persistence.Repositories; using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Security; @@ -506,6 +507,15 @@ public IEnumerable GetAll(int pageIndex, int pageSize, out int totalRecor } } + internal IEnumerable GetNextUsers(int id, int count) + { + var uow = UowProvider.GetUnitOfWork(); + using (var repository = (UserRepository) RepositoryFactory.CreateUserRepository(uow)) + { + return repository.GetNextUsers(id, count); + } + } + #endregion #region Implementation of IUserService diff --git a/src/Umbraco.Core/StringExtensions.cs b/src/Umbraco.Core/StringExtensions.cs index 036b5b979f19..27de06c3718a 100644 --- a/src/Umbraco.Core/StringExtensions.cs +++ b/src/Umbraco.Core/StringExtensions.cs @@ -184,7 +184,7 @@ internal static string ReplaceNonAlphanumericChars(this string input, char repla /// /// /// - internal static string CleanForXss(this string input, params char[] ignoreFromClean) + public static string CleanForXss(this string input, params char[] ignoreFromClean) { //remove any html input = input.StripHtml(); diff --git a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs index a417ec601a3c..7b713374aaad 100644 --- a/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs +++ b/src/Umbraco.Core/Sync/ApplicationUrlHelper.cs @@ -114,7 +114,8 @@ internal static bool TrySetApplicationUrl(ApplicationContext appContext, IUmbrac // - contain a scheme // - end or not with a slash, it will be taken care of // eg "http://www.mysite.com/umbraco" - var registrar = ServerRegistrarResolver.Current.Registrar as IServerRegistrar2; + var resolver = ServerRegistrarResolver.HasCurrent ? ServerRegistrarResolver.Current : null; + var registrar = resolver == null ? null : resolver.Registrar as IServerRegistrar2; url = registrar == null ? null : registrar.GetCurrentServerUmbracoApplicationUrl(); if (url.IsNullOrWhiteSpace() == false) { diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index 4e46c0ab5c43..12cb465b5013 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -29,7 +29,6 @@ namespace Umbraco.Core.Sync public class DatabaseServerMessenger : ServerMessengerBase { private readonly ApplicationContext _appContext; - private readonly DatabaseServerMessengerOptions _options; private readonly ManualResetEvent _syncIdle; private readonly object _locko = new object(); private readonly ILogger _logger; @@ -41,6 +40,7 @@ public class DatabaseServerMessenger : ServerMessengerBase private bool _released; private readonly ProfilingLogger _profilingLogger; + protected DatabaseServerMessengerOptions Options { get; private set; } protected ApplicationContext ApplicationContext { get { return _appContext; } } public DatabaseServerMessenger(ApplicationContext appContext, bool distributedEnabled, DatabaseServerMessengerOptions options) @@ -50,7 +50,7 @@ public DatabaseServerMessenger(ApplicationContext appContext, bool distributedEn if (options == null) throw new ArgumentNullException("options"); _appContext = appContext; - _options = options; + Options = options; _lastPruned = _lastSync = DateTime.UtcNow; _syncIdle = new ManualResetEvent(true); _profilingLogger = appContext.ProfilingLogger; @@ -115,7 +115,17 @@ protected void Boot() { _released = true; // no more syncs } - _syncIdle.WaitOne(); // wait for pending sync + + // wait a max of 5 seconds and then return, so that we don't block + // the entire MainDom callbacks chain and prevent the AppDomain from + // properly releasing MainDom - a timeout here means that one refresher + // is taking too much time processing, however when it's done we will + // not update lastId and stop everything + var idle =_syncIdle.WaitOne(5000); + if (idle == false) + { + _logger.Warn("The wait lock timed out, application is shutting down. The current instruction batch will be re-processed."); + } }, weight); @@ -154,14 +164,16 @@ private void Initialize() else { //check for how many instructions there are to process + //TODO: In 7.6 we need to store the count of instructions per row since this is not affective because there can be far more than one (if not thousands) + // of instructions in a single row. var count = _appContext.DatabaseContext.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoCacheInstruction WHERE id > @lastId", new {lastId = _lastId}); - if (count > _options.MaxProcessingInstructionCount) + if (count > Options.MaxProcessingInstructionCount) { //too many instructions, proceed to cold boot _logger.Warn("The instruction count ({0}) exceeds the specified MaxProcessingInstructionCount ({1})." + " The server will skip existing instructions, rebuild its caches and indexes entirely, adjust its last synced Id" + " to the latest found in the database and maintain cache updates based on that Id.", - () => count, () => _options.MaxProcessingInstructionCount); + () => count, () => Options.MaxProcessingInstructionCount); coldboot = true; } @@ -179,8 +191,8 @@ private void Initialize() SaveLastSynced(maxId); // execute initializing callbacks - if (_options.InitializingCallbacks != null) - foreach (var callback in _options.InitializingCallbacks) + if (Options.InitializingCallbacks != null) + foreach (var callback in Options.InitializingCallbacks) callback(); } @@ -198,12 +210,14 @@ protected void Sync() if (_syncing) return; + //Don't continue if we are released if (_released) return; - if ((DateTime.UtcNow - _lastSync).TotalSeconds <= _options.ThrottleSeconds) + if ((DateTime.UtcNow - _lastSync).TotalSeconds <= Options.ThrottleSeconds) return; + //Set our flag and the lock to be in it's original state (i.e. it can be awaited) _syncing = true; _syncIdle.Reset(); _lastSync = DateTime.UtcNow; @@ -215,7 +229,8 @@ protected void Sync() { ProcessDatabaseInstructions(); - if ((DateTime.UtcNow - _lastPruned).TotalSeconds <= _options.PruneThrottleSeconds) + //Check for pruning throttling + if ((_released || (DateTime.UtcNow - _lastPruned).TotalSeconds <= Options.PruneThrottleSeconds)) return; _lastPruned = _lastSync; @@ -231,7 +246,12 @@ protected void Sync() } finally { - _syncing = false; + lock (_locko) + { + //We must reset our flag and signal any waiting locks + _syncing = false; + } + _syncIdle.Set(); } } @@ -255,13 +275,17 @@ private void ProcessDatabaseInstructions() // // FIXME not true if we're running on a background thread, assuming we can? + var sql = new Sql().Select("*") .From(_appContext.DatabaseContext.SqlSyntax) .Where(dto => dto.Id > _lastId) .OrderBy(dto => dto.Id, _appContext.DatabaseContext.SqlSyntax); - var dtos = _appContext.DatabaseContext.Database.Fetch(sql); - if (dtos.Count <= 0) return; + //only retrieve the top 100 (just in case there's tons) + // even though MaxProcessingInstructionCount is by default 1000 we still don't want to process that many + // rows in one request thread since each row can contain a ton of instructions (until 7.5.5 in which case + // a row can only contain MaxProcessingInstructionCount) + var topSql = _appContext.DatabaseContext.SqlSyntax.SelectTop(sql, 100); // only process instructions coming from a remote server, and ignore instructions coming from // the local server as they've already been processed. We should NOT assume that the sequence of @@ -269,8 +293,22 @@ private void ProcessDatabaseInstructions() var localIdentity = LocalIdentity; var lastId = 0; - foreach (var dto in dtos) + + //tracks which ones have already been processed to avoid duplicates + var processed = new HashSet(); + + //It would have been nice to do this in a Query instead of Fetch using a data reader to save + // some memory however we cannot do thta because inside of this loop the cache refreshers are also + // performing some lookups which cannot be done with an active reader open + foreach (var dto in _appContext.DatabaseContext.Database.Fetch(topSql)) { + //If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot + // continue processing anything otherwise we'll hold up the app domain shutdown + if (_released) + { + break; + } + if (dto.OriginIdentity == localIdentity) { // just skip that local one but update lastId nevertheless @@ -291,27 +329,69 @@ private void ProcessDatabaseInstructions() continue; } - // execute remote instructions & update lastId - try + var instructionBatch = GetAllInstructions(jsonA); + + //process as per-normal + var success = ProcessDatabaseInstructions(instructionBatch, dto, processed, ref lastId); + + //if they couldn't be all processed (i.e. we're shutting down) then exit + if (success == false) { - NotifyRefreshers(jsonA); - lastId = dto.Id; + _logger.Info("The current batch of instructions was not processed, app is shutting down"); + break; } - catch (Exception ex) - { - _logger.Error( - string.Format("DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions ({0}: \"{1}\"). Instruction is being skipped/ignored", dto.Id, dto.Instructions), ex); - //we cannot throw here because this invalid instruction will just keep getting processed over and over and errors - // will be thrown over and over. The only thing we can do is ignore and move on. - lastId = dto.Id; - } } if (lastId > 0) SaveLastSynced(lastId); } + /// + /// Processes the instruction batch and checks for errors + /// + /// + /// + /// + /// Tracks which instructions have already been processed to avoid duplicates + /// + /// + /// + /// returns true if all instructions in the batch were processed, otherwise false if they could not be due to the app being shut down + /// + private bool ProcessDatabaseInstructions(IReadOnlyCollection instructionBatch, CacheInstructionDto dto, HashSet processed, ref int lastId) + { + // execute remote instructions & update lastId + try + { + var result = NotifyRefreshers(instructionBatch, processed); + if (result) + { + //if all instructions we're processed, set the last id + lastId = dto.Id; + } + return result; + } + //catch (ThreadAbortException ex) + //{ + // //This will occur if the instructions processing is taking too long since this is occuring on a request thread. + // // Or possibly if IIS terminates the appdomain. In any case, we should deal with this differently perhaps... + //} + catch (Exception ex) + { + _logger.Error( + string.Format("DISTRIBUTED CACHE IS NOT UPDATED. Failed to execute instructions (id: {0}, instruction count: {1}). Instruction is being skipped/ignored", dto.Id, instructionBatch.Count), ex); + + //we cannot throw here because this invalid instruction will just keep getting processed over and over and errors + // will be thrown over and over. The only thing we can do is ignore and move on. + lastId = dto.Id; + return false; + } + + ////if this is returned it will not be saved + //return -1; + } + /// /// Remove old instructions from the database /// @@ -322,7 +402,7 @@ private void ProcessDatabaseInstructions() /// private void PruneOldInstructions() { - var pruneDate = DateTime.UtcNow.AddDays(-_options.DaysToRetainInstructions); + var pruneDate = DateTime.UtcNow.AddDays(-Options.DaysToRetainInstructions); // using 2 queries is faster than convoluted joins @@ -459,8 +539,14 @@ private static IJsonCacheRefresher GetJsonRefresher(ICacheRefresher refresher) return jsonRefresher; } - private static void NotifyRefreshers(IEnumerable jsonArray) + /// + /// Parses out the individual instructions to be processed + /// + /// + /// + private static List GetAllInstructions(IEnumerable jsonArray) { + var result = new List(); foreach (var jsonItem in jsonArray) { // could be a JObject in which case we can convert to a RefreshInstruction, @@ -469,35 +555,64 @@ private static void NotifyRefreshers(IEnumerable jsonArray) if (jsonObj != null) { var instruction = jsonObj.ToObject(); - switch (instruction.RefreshType) - { - case RefreshMethodType.RefreshAll: - RefreshAll(instruction.RefresherId); - break; - case RefreshMethodType.RefreshByGuid: - RefreshByGuid(instruction.RefresherId, instruction.GuidId); - break; - case RefreshMethodType.RefreshById: - RefreshById(instruction.RefresherId, instruction.IntId); - break; - case RefreshMethodType.RefreshByIds: - RefreshByIds(instruction.RefresherId, instruction.JsonIds); - break; - case RefreshMethodType.RefreshByJson: - RefreshByJson(instruction.RefresherId, instruction.JsonPayload); - break; - case RefreshMethodType.RemoveById: - RemoveById(instruction.RefresherId, instruction.IntId); - break; - } - + result.Add(instruction); } else { - var jsonInnerArray = (JArray) jsonItem; - NotifyRefreshers(jsonInnerArray); // recurse + var jsonInnerArray = (JArray)jsonItem; + result.AddRange(GetAllInstructions(jsonInnerArray)); // recurse } } + return result; + } + + /// + /// executes the instructions against the cache refresher instances + /// + /// + /// + /// + /// Returns true if all instructions were processed, otherwise false if the processing was interupted (i.e. app shutdown) + /// + private bool NotifyRefreshers(IEnumerable instructions, HashSet processed) + { + foreach (var instruction in instructions) + { + //Check if the app is shutting down, we need to exit if this happens. + if (_released) + { + return false; + } + + //this has already been processed + if (processed.Contains(instruction)) + continue; + + switch (instruction.RefreshType) + { + case RefreshMethodType.RefreshAll: + RefreshAll(instruction.RefresherId); + break; + case RefreshMethodType.RefreshByGuid: + RefreshByGuid(instruction.RefresherId, instruction.GuidId); + break; + case RefreshMethodType.RefreshById: + RefreshById(instruction.RefresherId, instruction.IntId); + break; + case RefreshMethodType.RefreshByIds: + RefreshByIds(instruction.RefresherId, instruction.JsonIds); + break; + case RefreshMethodType.RefreshByJson: + RefreshByJson(instruction.RefresherId, instruction.JsonPayload); + break; + case RefreshMethodType.RemoveById: + RemoveById(instruction.RefresherId, instruction.IntId); + break; + } + + processed.Add(instruction); + } + return true; } private static void RefreshAll(Guid uniqueIdentifier) diff --git a/src/Umbraco.Core/Sync/SingleServerRegistrar.cs b/src/Umbraco.Core/Sync/SingleServerRegistrar.cs new file mode 100644 index 000000000000..117ce516be32 --- /dev/null +++ b/src/Umbraco.Core/Sync/SingleServerRegistrar.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Sync +{ + public class SingleServerRegistrar : IServerRegistrar2 + { + private readonly string _umbracoApplicationUrl; + + public IEnumerable Registrations { get; private set; } + + public SingleServerRegistrar() + { + _umbracoApplicationUrl = ApplicationContext.Current.UmbracoApplicationUrl; + Registrations = new[] { new ServerAddressImpl(_umbracoApplicationUrl) }; + } + + public ServerRole GetCurrentServerRole() + { + return ServerRole.Single; + } + + public string GetCurrentServerUmbracoApplicationUrl() + { + return _umbracoApplicationUrl; + } + + private class ServerAddressImpl : IServerAddress + { + public ServerAddressImpl(string serverAddress) + { + ServerAddress = serverAddress; + } + + public string ServerAddress { get; private set; } + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index e6e91549fa52..e0eb63b26553 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -37,12 +37,12 @@ false - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll True - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True @@ -52,8 +52,8 @@ ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - ..\packages\ImageProcessor.2.4.4.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.4.5.0\lib\net45\ImageProcessor.dll True @@ -415,6 +415,7 @@ + @@ -428,6 +429,7 @@ + @@ -438,6 +440,7 @@ + @@ -472,6 +475,8 @@ + + @@ -1013,10 +1018,10 @@ - - + + - + @@ -1198,7 +1203,7 @@ - + @@ -1330,6 +1335,7 @@ + @@ -1452,6 +1458,7 @@ + + \ No newline at end of file diff --git a/src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs new file mode 100644 index 000000000000..c12545ec9466 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/XmlBenchmarks.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Xml; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Diagnostics.Windows; +using BenchmarkDotNet.Jobs; + +namespace Umbraco.Tests.Benchmarks +{ + [Config(typeof(Config))] + public class XmlBenchmarks + { + private class Config : ManualConfig + { + public Config() + { + Add(new MemoryDiagnoser()); + //Add(ExecutionValidator.FailOnError); + + //The 'quick and dirty' settings, so it runs a little quicker + // see benchmarkdotnet FAQ + Add(Job.Default + .WithLaunchCount(1) // benchmark process will be launched only once + .WithIterationTime(100) // 100ms per iteration + .WithWarmupCount(3) // 3 warmup iteration + .WithTargetCount(3)); // 3 target iteration + } + } + + [Setup] + public void Setup() + { + var templateId = 0; + var xmlText = @" + + + + +]> + + + + + 1 + + This is some content]]> + + + + + + + + + + + + + + + + +"; + _xml = new XmlDocument(); + _xml.LoadXml(xmlText); + } + + [Cleanup] + public void Cleanup() + { + _xml = null; + } + + private XmlDocument _xml; + + [Benchmark] + public void XmlWithXPath() + { + var xpath = "/root/* [@isDoc and @urlName='home']//* [@isDoc and @urlName='sub1']//* [@isDoc and @urlName='sub2']"; + var elt = _xml.SelectSingleNode(xpath); + if (elt == null) Console.WriteLine("ERR"); + } + + [Benchmark] + public void XmlWithNavigation() + { + var elt = _xml.DocumentElement; + var id = NavigateElementRoute(elt, new[] {"home", "sub1", "sub2"}); + if (id <= 0) Console.WriteLine("ERR"); + } + + private const bool UseLegacySchema = false; + + private int NavigateElementRoute(XmlElement elt, string[] urlParts) + { + var found = true; + var i = 0; + while (found && i < urlParts.Length) + { + found = false; + foreach (XmlElement child in elt.ChildNodes) + { + var noNode = UseLegacySchema + ? child.Name != "node" + : child.GetAttributeNode("isDoc") == null; + if (noNode) continue; + if (child.GetAttribute("urlName") != urlParts[i]) continue; + + found = true; + elt = child; + break; + } + i++; + } + return found ? int.Parse(elt.GetAttribute("id")) : -1; + } + } +} diff --git a/src/Umbraco.Tests.Benchmarks/packages.config b/src/Umbraco.Tests.Benchmarks/packages.config new file mode 100644 index 000000000000..c4d2ba1df251 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/packages.config @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs b/src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs new file mode 100644 index 000000000000..b1e1a79ddb66 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/BulkDataReaderTests.cs @@ -0,0 +1,2432 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Data.Common; +using System.Data.SqlClient; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Persistence; + +namespace Umbraco.Tests.Persistence +{ + /// + /// Unit tests for . + /// + /// + /// Borrowed from Microsoft: + /// See: https://blogs.msdn.microsoft.com/anthonybloesch/2013/01/23/bulk-loading-data-with-idatareader-and-sqlbulkcopy/ + /// + [TestFixture] + public class BulkDataReaderTest + { + + #region Test constants + + /// + /// The schema name. + /// + private const string testSchemaName = "TestSchema"; + + /// + /// The table name. + /// + private const string testTableName = "TestTable"; + + /// + /// The test UDT schema name. + /// + private const string testUdtSchemaName = "UdtSchema"; + + /// + /// The test UDT name. + /// + private const string testUdtName = "TestUdt"; + + /// + /// The test XML schema collection database name. + /// + private const string testXmlSchemaCollectionDatabaseName = "XmlDatabase"; + + /// + /// The test XML schema collection owning schema name. + /// + private const string testXMLSchemaCollectionSchemaName = "XmlSchema"; + + /// + /// The test XML schema collection name. + /// + private const string testXMLSchemaCollectionName = "Xml"; + + #endregion + + #region Schema tests + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void ColumnMappingsTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + ReadOnlyCollection columnMappings = testReader.ColumnMappings; + + Assert.IsTrue(columnMappings.Count > 0); + Assert.AreEqual(columnMappings.Count, testReader.FieldCount); + + foreach (SqlBulkCopyColumnMapping columnMapping in columnMappings) + { + Assert.AreEqual(columnMapping.SourceColumn, columnMapping.DestinationColumn); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void GetDataTypeNameTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.FieldCount > 0); + + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.GetDataTypeName(currentColumn), ((Type)testReader.GetSchemaTable().Rows[currentColumn][SchemaTableColumn.DataType]).Name); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void GetFieldTypeTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.FieldCount > 0); + + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.GetFieldType(currentColumn), testReader.GetSchemaTable().Rows[currentColumn][SchemaTableColumn.DataType]); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void GetOrdinalTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.FieldCount > 0); + + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.GetOrdinal(testReader.GetName(currentColumn)), currentColumn); + + Assert.AreEqual(testReader.GetOrdinal(testReader.GetName(currentColumn).ToUpperInvariant()), currentColumn); + } + } + } + + /// + /// Test that functions correctly. + /// + /// + /// uses to test legal schema combinations. + /// + /// + [Test] + public void GetSchemaTableTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.IsNotNull(schemaTable); + Assert.IsTrue(schemaTable.Rows.Count > 0); + Assert.AreEqual(schemaTable.Rows.Count, BulkDataReaderSubclass.ExpectedResultSet.Count); + } + } + + /// + /// Test that + /// throws a for null column names. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowNullColumnNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = null; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.BigInt; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty column names. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowEmptyColumnNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = string.Empty; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.BigInt; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nonpositive column sizes. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNonpositiveColumnSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 0; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NVarChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nonpositive numeric precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNonpositiveNumericPrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 0; + testReader.NumericScale = 0; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for negative numeric scale. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNegativeNumericScaleTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = -1; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for binary column without a column size. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowBinaryWithoutSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Binary; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for binary column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowBinaryWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Binary; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for char column without a column size. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowCharWithoutSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Char; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for char column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Char; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column without a column precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowDecimalWithoutPrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = 5; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column without a column scale. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowDecimalWithoutScaleTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 20; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column with a column precision that is too large (>38). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowDecimalWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 39; + testReader.NumericScale = 5; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for decimal column with a column scale that is larger than the column precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowDecimalWithTooLargeScaleTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 20; + testReader.NumericScale = 21; + testReader.ProviderType = SqlDbType.Decimal; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for datetime2 column with a column size that has a precision that is too large (>7). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowDateTime2WithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 8; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.DateTime2; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for datetimeoffset column with a column size that has a precision that is too large (>7). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowDateTimeOffsetWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 8; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.DateTimeOffset; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nchar column without a precision. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowFloatWithoutPrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Float; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for float column with a column precision that is too large (>53). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowFloatWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 54; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Float; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nchar column without a column size. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowNCharWithoutSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nchar column with a column size that is too large (>4000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 4001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for nvarchar column with a column size that is too large (>4000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowNVarCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 4001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.NVarChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for time column with a column precision that is too large (>7). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowTimeWithTooLargePrecisionTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 8; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Time; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for missing UDT schema name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtMissingSchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = null; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty UDT schema name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtEmptySchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = string.Empty; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for missing UDT name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtMissingNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = "Schema"; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty UDT name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowUdtEmptyNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Udt; + testReader.UdtSchema = "Schema"; + testReader.UdtType = string.Empty; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for varbinary column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowVarBinaryWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.VarBinary; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for varchar column with a column size that is too large (>8000). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowVarCharWithTooLargeSizeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 8001; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.VarChar; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for null xml collection name but with a name for the database. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlNullNameWithDatabaseNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for null xml collection name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlNullNameWithOwningSchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty xml collection database name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlEmptyDatabaseNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = string.Empty; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = "Xml"; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty xml collection owning schema name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlEmptyOwningSchemaNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = string.Empty; + testReader.XmlSchemaCollectionName = "Xml"; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for empty xml collection name. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentException))] + public void AddSchemaTableRowXmlEmptyNameTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Xml; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = string.Empty; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for a structured column (which is illegal). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowStructuredTypeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Structured; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws a for a timestamp column (which is illegal). + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void AddSchemaTableRowTimestampTypeTest() + { + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.ProviderType = SqlDbType.Timestamp; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + DataTable schemaTable = testReader.GetSchemaTable(); ; + } + } + + /// + /// Test that + /// throws an for a column with an unallowed optional column set. + /// + /// + /// Uses to test the illegal schema combination. + /// + /// + [Test] + public void AddSchemaTableRowUnallowedOptionalColumnTest() + { + + // Column size set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = 5; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Bit, SqlDbType.Date, SqlDbType.DateTime, SqlDbType.DateTime2, + SqlDbType.DateTimeOffset, SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.Real, + SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, + SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, SqlDbType.Udt, SqlDbType.UniqueIdentifier, + SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // Numeric precision set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, + SqlDbType.NText, SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, + SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.Udt, SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, + SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // Numeric scale set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = 3; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Image, SqlDbType.Int, + SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, SqlDbType.Real, + SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, + SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, SqlDbType.Udt, SqlDbType.UniqueIdentifier, + SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // Numeric scale set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = 5; + testReader.NumericScale = 3; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Image, SqlDbType.Int, + SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, SqlDbType.NVarChar, SqlDbType.Real, + SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, SqlDbType.Structured, SqlDbType.Text, + SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, SqlDbType.Udt, SqlDbType.UniqueIdentifier, + SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // UDT type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // UDT schema and type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = "Schema"; + testReader.UdtType = "Type"; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = null; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Xml }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // XML type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = null; + testReader.XmlSchemaCollectionName = "Name"; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Udt }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // XML owning schema and type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = null; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = "Name"; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Udt }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + + // XML database, owning schema and type name set + using (BulkDataReaderSchemaTest testReader = new BulkDataReaderSchemaTest()) + { + testReader.AllowDBNull = false; + testReader.ColumnName = "Name"; + testReader.ColumnSize = null; + testReader.IsKey = false; + testReader.IsUnique = false; + testReader.NumericPrecision = null; + testReader.NumericScale = null; + testReader.UdtSchema = null; + testReader.UdtType = null; + testReader.XmlSchemaCollectionDatabase = "Database"; + testReader.XmlSchemaCollectionOwningSchema = "Schema"; + testReader.XmlSchemaCollectionName = "Name"; + + foreach (SqlDbType dbtype in new List { SqlDbType.BigInt, SqlDbType.Binary, SqlDbType.Bit, SqlDbType.Char, SqlDbType.Date, + SqlDbType.DateTime, SqlDbType.DateTime2, SqlDbType.DateTimeOffset, SqlDbType.Decimal, SqlDbType.Float, + SqlDbType.Image, SqlDbType.Int, SqlDbType.Money, SqlDbType.NChar, SqlDbType.NText, + SqlDbType.NVarChar, SqlDbType.Real, SqlDbType.SmallDateTime, SqlDbType.SmallInt, SqlDbType.SmallMoney, + SqlDbType.Structured, SqlDbType.Text, SqlDbType.Time, SqlDbType.Timestamp, SqlDbType.TinyInt, + SqlDbType.UniqueIdentifier, SqlDbType.VarBinary, SqlDbType.VarChar, SqlDbType.Variant, SqlDbType.Udt }) + { + testReader.ProviderType = dbtype; + + try + { + DataTable schemaTable = testReader.GetSchemaTable(); + + Assert.Fail(); + } + catch (ArgumentException) + { + } + } + } + } + + #endregion; + + #region Rowset tests + + /// + /// Test that is functioning correctly. + /// + /// + [Test] + public void CloseTest() + { + BulkDataReaderSubclass testReader = new BulkDataReaderSubclass(); + + testReader.Close(); + + Assert.IsTrue(testReader.IsClosed); + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because nested row sets are not supported, this should always return 0; + /// + /// + [Test] + public void DepthTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + Assert.AreEqual(testReader.Depth, 0); + } + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because nested row sets are not supported, this should always return null; + /// + /// + [Test] + public void GetDataTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + Assert.IsTrue(testReader.FieldCount > 0); + + Assert.IsNull(testReader.GetData(0)); + } + } + + /// + /// Test and related functions. + /// + /// + /// Uses to test legal schema combinations. + /// + [Test] + public void GetValueTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + // this[int] + for (int column = 0; column < BulkDataReaderSubclass.ExpectedResultSet.Count; column++) + { + Assert.AreEqual(testReader[column], BulkDataReaderSubclass.ExpectedResultSet[column]); + } + + // this[string] + for (int column = 0; column < BulkDataReaderSubclass.ExpectedResultSet.Count; column++) + { + Assert.AreEqual(testReader[testReader.GetName(column)], BulkDataReaderSubclass.ExpectedResultSet[column]); + + Assert.AreEqual(testReader[testReader.GetName(column).ToUpperInvariant()], BulkDataReaderSubclass.ExpectedResultSet[column]); + } + + // GetValues + { + object[] values = new object[BulkDataReaderSubclass.ExpectedResultSet.Count]; + object[] expectedValues = new object[BulkDataReaderSubclass.ExpectedResultSet.Count]; + + Assert.AreEqual(testReader.GetValues(values), values.Length); + + BulkDataReaderSubclass.ExpectedResultSet.CopyTo(expectedValues, 0); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(values, expectedValues)); + } + + // Typed getters + { + int currentColumn = 0; + + Assert.AreEqual(testReader.GetInt64(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + Assert.AreEqual(testReader.GetBoolean(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.IsDBNull(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn] == null); + currentColumn++; + + Assert.AreEqual(testReader.GetChar(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetChar(currentColumn), ((char[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn])[0]); + currentColumn++; + + Assert.AreEqual(testReader.GetChar(currentColumn), ((string)BulkDataReaderSubclass.ExpectedResultSet[currentColumn])[0]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + + { + char[] expectedResult = ((string)BulkDataReaderSubclass.ExpectedResultSet[currentColumn]).ToCharArray(); + int expectedLength = expectedResult.Length; + char[] buffer = new char[expectedLength]; + + Assert.AreEqual(testReader.GetChars(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTimeOffset(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTimeOffset(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDecimal(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDouble(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + Assert.AreEqual(testReader.GetInt32(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDecimal(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetFloat(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDateTime(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetInt16(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetDecimal(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetTimeSpan(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetTimeSpan(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetByte(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetValue(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetGuid(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + { + byte[] expectedResult = (byte[])BulkDataReaderSubclass.ExpectedResultSet[currentColumn]; + int expectedLength = expectedResult.Length; + byte[] buffer = new byte[expectedLength]; + + Assert.AreEqual(testReader.GetBytes(currentColumn, 0, buffer, 0, expectedLength), expectedLength); + + Assert.IsTrue(BulkDataReaderTest.ArraysMatch(buffer, expectedResult)); + } + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetValue(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + + Assert.AreEqual(testReader.GetString(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn]); + currentColumn++; + } + } + } + + /// + /// Test throws a when + /// the index is too small. + /// + /// + /// Uses to test the method. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetValueIndexTooSmallTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetValue(-1); + } + } + + /// + /// Test throws a when + /// the index is too large. + /// + /// + /// Uses to test the method. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetValueIndexTooLargeTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetValue(testReader.FieldCount); + } + } + + /// + /// Test throws a when + /// the index is too small. + /// + /// + /// Uses to test the method. + /// + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetDataIndexTooSmallTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetData(-1); + } + } + + /// + /// Test throws a when + /// the index is too large. + /// + /// + /// Uses to test the method. + /// + [Test] + [ExpectedException(typeof(ArgumentOutOfRangeException))] + public void GetDataIndexTooLargeTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + object result = testReader.GetData(testReader.FieldCount); + } + } + + /// + /// Test that functions correctly. + /// + /// + [Test] + public void IsDBNullTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + for (int currentColumn = 0; currentColumn < testReader.FieldCount; currentColumn++) + { + Assert.AreEqual(testReader.IsDBNull(currentColumn), BulkDataReaderSubclass.ExpectedResultSet[currentColumn] == null); + } + } + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because this is a single row set, this should always return false; + /// + /// + [Test] + public void NextResultTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsFalse(testReader.NextResult()); + } + } + + /// + /// Test that is functioning correctly. + /// + /// + /// Because this row set represents a data source, this should always return -1; + /// + /// + [Test] + public void RecordsAffectedTest() + { + using (BulkDataReaderSubclass testReader = new BulkDataReaderSubclass()) + { + Assert.IsTrue(testReader.Read()); + + Assert.AreEqual(testReader.RecordsAffected, -1); + } + } + + #endregion + + #region Test IDisposable + + /// + /// Test that the interface is functioning correctly. + /// + /// + /// + [Test] + public void IDisposableTest() + { + // Test the Dispose method + { + BulkDataReaderSubclass testReader = new BulkDataReaderSubclass(); + + testReader.Dispose(); + + Assert.IsTrue(testReader.IsClosed); + } + + // Test the finalizer method + { + BulkDataReaderSubclass testReader = new BulkDataReaderSubclass(); + + testReader = null; + + GC.Collect(); + + GC.WaitForPendingFinalizers(); + } + } + + #endregion + + #region Utility + + /// + /// Do the two arrays match exactly? + /// + /// + /// The type of the array elements. + /// + /// + /// The first array. + /// + /// + /// The second array. + /// + /// + /// True if the arrays have the same length and contents. + /// + private static bool ArraysMatch(ElementType[] left, + ElementType[] right) + { + if (left == null) + { + throw new ArgumentNullException("left"); + } + else if (right == null) + { + throw new ArgumentNullException("left"); + } + + bool result = true; + + if (left.Length != right.Length) + { + result = false; + } + else + { + for (int currentIndex = 0; currentIndex < left.Length; currentIndex++) + { + result &= object.Equals(left[currentIndex], right[currentIndex]); + } + } + + return result; + } + + #endregion + + #region Test stubs + + /// + /// A subclass of used for testing its utility functions. + /// + private class BulkDataReaderSubclass : BulkDataReader + { + + #region Constructors + + /// + /// Constructor. + /// + public BulkDataReaderSubclass() + { + } + + #endregion + + #region BulkDataReader + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string SchemaName + { + get { return BulkDataReaderTest.testSchemaName; } + } + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string TableName + { + get { return BulkDataReaderTest.testTableName; } + } + + /// + /// See + /// + /// + /// Creates a schema row for the various values. + /// + protected override void AddSchemaTableRows() + { + AddSchemaTableRow("BigInt", null, null, null, true, false, false, SqlDbType.BigInt, null, null, null, null, null); + AddSchemaTableRow("Binary_20", 20, null, null, false, true, false, SqlDbType.Binary, null, null, null, null, null); + AddSchemaTableRow("Bit", null, null, null, false, false, true, SqlDbType.Bit, null, null, null, null, null); + AddSchemaTableRow("Bit_null", null, null, null, false, false, true, SqlDbType.Bit, null, null, null, null, null); + AddSchemaTableRow("Char_Char", 1, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Char_Char_Array", 1, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Char_String", 1, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Char_20_String", 20, null, null, false, false, false, SqlDbType.Char, null, null, null, null, null); + AddSchemaTableRow("Date", null, null, null, false, false, false, SqlDbType.Date, null, null, null, null, null); + AddSchemaTableRow("DateTime", null, null, null, false, false, false, SqlDbType.DateTime, null, null, null, null, null); + AddSchemaTableRow("DateTime2", null, null, null, false, false, false, SqlDbType.DateTime2, null, null, null, null, null); + AddSchemaTableRow("DateTime2_5", null, 5, null, false, false, false, SqlDbType.DateTime2, null, null, null, null, null); + AddSchemaTableRow("DateTimeOffset", null, null, null, false, false, false, SqlDbType.DateTimeOffset, null, null, null, null, null); + AddSchemaTableRow("DateTimeOffset_5", null, 5, null, false, false, false, SqlDbType.DateTimeOffset, null, null, null, null, null); + AddSchemaTableRow("Decimal_20_10", null, 20, 10, false, false, false, SqlDbType.Decimal, null, null, null, null, null); + AddSchemaTableRow("Float_50", null, 50, null, false, false, false, SqlDbType.Float, null, null, null, null, null); + AddSchemaTableRow("Image", null, null, null, false, false, false, SqlDbType.Image, null, null, null, null, null); + AddSchemaTableRow("Int", null, null, null, false, false, false, SqlDbType.Int, null, null, null, null, null); + AddSchemaTableRow("Money", null, null, null, false, false, false, SqlDbType.Money, null, null, null, null, null); + AddSchemaTableRow("NChar_20", 20, null, null, false, false, false, SqlDbType.NChar, null, null, null, null, null); + AddSchemaTableRow("NText", null, null, null, false, false, false, SqlDbType.NText, null, null, null, null, null); + AddSchemaTableRow("NVarChar_20", 20, null, null, false, false, false, SqlDbType.NVarChar, null, null, null, null, null); + AddSchemaTableRow("NVarChar_Max", null, null, null, false, false, false, SqlDbType.NVarChar, null, null, null, null, null); + AddSchemaTableRow("Real", null, null, null, false, false, false, SqlDbType.Real, null, null, null, null, null); + AddSchemaTableRow("SmallDateTime", null, null, null, false, false, false, SqlDbType.SmallDateTime, null, null, null, null, null); + AddSchemaTableRow("SmallInt", null, null, null, false, false, false, SqlDbType.SmallInt, null, null, null, null, null); + AddSchemaTableRow("SmallMoney", null, null, null, false, false, false, SqlDbType.SmallMoney, null, null, null, null, null); + AddSchemaTableRow("Text", null, null, null, false, false, false, SqlDbType.Text, null, null, null, null, null); + AddSchemaTableRow("Time", null, null, null, false, false, false, SqlDbType.Time, null, null, null, null, null); + AddSchemaTableRow("Time_5", null, 5, null, false, false, false, SqlDbType.Time, null, null, null, null, null); + AddSchemaTableRow("TinyInt", null, null, null, false, false, false, SqlDbType.TinyInt, null, null, null, null, null); + AddSchemaTableRow("Udt", null, null, null, false, false, false, SqlDbType.Udt, BulkDataReaderTest.testUdtSchemaName, BulkDataReaderTest.testUdtName, null, null, null); + AddSchemaTableRow("UniqueIdentifier", null, null, null, false, false, false, SqlDbType.UniqueIdentifier, null, null, null, null, null); + AddSchemaTableRow("VarBinary_20", 20, null, null, false, false, false, SqlDbType.VarBinary, null, null, null, null, null); + AddSchemaTableRow("VarBinary_Max", null, null, null, false, false, false, SqlDbType.VarBinary, null, null, null, null, null); + AddSchemaTableRow("VarChar_20", 20, null, null, false, false, false, SqlDbType.VarChar, null, null, null, null, null); + AddSchemaTableRow("VarChar_Max", null, null, null, false, false, false, SqlDbType.VarChar, null, null, null, null, null); + AddSchemaTableRow("Variant", null, null, null, false, false, false, SqlDbType.Variant, null, null, null, null, null); + AddSchemaTableRow("Xml_Database", null, null, null, false, false, false, SqlDbType.Xml, null, null, BulkDataReaderTest.testXmlSchemaCollectionDatabaseName, BulkDataReaderTest.testXMLSchemaCollectionSchemaName, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml_Database_XML", null, null, null, false, false, false, SqlDbType.Xml, null, null, BulkDataReaderTest.testXmlSchemaCollectionDatabaseName, BulkDataReaderTest.testXMLSchemaCollectionSchemaName, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml_Schema", null, null, null, false, false, false, SqlDbType.Xml, null, null, null, BulkDataReaderTest.testXMLSchemaCollectionSchemaName, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml_Xml", null, null, null, false, false, false, SqlDbType.Xml, null, null, null, null, BulkDataReaderTest.testXMLSchemaCollectionName); + AddSchemaTableRow("Xml", null, null, null, false, false, false, SqlDbType.Xml, null, null, null, null, null); + } + + /// + /// The result set returned by the . + /// + public static readonly ReadOnlyCollection ExpectedResultSet = new ReadOnlyCollection(new List + { + (long)10, + new byte[20], + true, + null, + 'c', + new char[] { 'c' }, + "c", + "char 20", + DateTime.UtcNow, + DateTime.UtcNow, + DateTime.UtcNow, + DateTime.UtcNow, + DateTimeOffset.UtcNow, + DateTimeOffset.UtcNow, + (decimal)10.5, + (double)10.5, + new byte[20], + (int)10, + (decimal)10.5, + "nchar 20", + "ntext", + "nvarchar 20", + "nvarchar max", + (float)10.5, + DateTime.UtcNow, + (short)10, + (decimal)10.5, + "text", + DateTime.UtcNow.TimeOfDay, + DateTime.UtcNow.TimeOfDay, + (byte)10, + new object(), + Guid.NewGuid(), + new byte[20], + new byte[20], + "varchar 20", + "varchar max", + (int)10, + @"", + @"", + @"", + @"", + @"" + }); + + /// + /// See + /// + /// + /// The zero-based column ordinal. + /// + /// + /// The value of the column in . + /// + /// + public override object GetValue(int i) + { + return BulkDataReaderSubclass.ExpectedResultSet[i]; + } + + /// + /// The number of rows read. + /// + private int readCount = 0; + + /// + /// See + /// + /// + /// True if there are more rows; otherwise, false. + /// + /// + public override bool Read() + { + return readCount++ < 1; + } + + #endregion + + } + + private class BulkDataReaderSchemaTest : BulkDataReader + { + + #region Properties + + /// + /// Is the column nullable (i.e. optional)? + /// + public bool AllowDBNull { get; set; } + + /// + /// The name of the column. + /// + public string ColumnName { get; set; } + + /// + /// The size of the column which may be null if not applicable. + /// + public int? ColumnSize { get; set; } + + /// + /// Is the column part of the primary key? + /// + public bool IsKey { get; set; } + + /// + /// Are the column values unique (i.e. never duplicated)? + /// + public bool IsUnique { get; set; } + + /// + /// The precision of the column which may be null if not applicable. + /// + public short? NumericPrecision { get; set; } + + /// + /// The scale of the column which may be null if not applicable. + /// + public short? NumericScale { get; set; } + + /// + /// The corresponding . + /// + public SqlDbType ProviderType { get; set; } + + /// + /// The schema name of the UDT. + /// + public string UdtSchema { get; set; } + + /// + /// The type name of the UDT. + /// + public string UdtType { get; set; } + + /// + /// For XML columns the schema collection's database name. Otherwise, null. + /// + public string XmlSchemaCollectionDatabase { get; set; } + + /// + /// For XML columns the schema collection's name. Otherwise, null. + /// + public string XmlSchemaCollectionName { get; set; } + + /// + /// For XML columns the schema collection's schema name. Otherwise, null. + /// + public string XmlSchemaCollectionOwningSchema { get; set; } + + #endregion + + #region Constructors + + /// + /// Constructor. + /// + public BulkDataReaderSchemaTest() + { + } + + #endregion + + #region BulkDataReader + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string SchemaName + { + get { return BulkDataReaderTest.testSchemaName; } + } + + /// + /// See . + /// + /// + /// Returns . + /// + protected override string TableName + { + get { return BulkDataReaderTest.testTableName; } + } + + /// + /// See + /// + /// + /// Creates a schema row for the various values. + /// + protected override void AddSchemaTableRows() + { + AddSchemaTableRow(this.ColumnName, + this.ColumnSize, + this.NumericPrecision, + this.NumericScale, + this.IsUnique, + this.IsKey, + this.AllowDBNull, + this.ProviderType, + this.UdtSchema, + this.UdtType, + this.XmlSchemaCollectionDatabase, + this.XmlSchemaCollectionOwningSchema, + this.XmlSchemaCollectionName); + } + + /// + /// See + /// + /// + /// The test stub is only for testing schema functionality and behaves as if it has no rows. + /// + /// + /// The zero-based column ordinal. + /// + /// + /// Never returns. + /// + /// + public override object GetValue(int i) + { + throw new InvalidOperationException("No data."); + } + + + /// + /// See + /// + /// + /// False. + /// + /// + public override bool Read() + { + return false; + } + + #endregion + + } + + #endregion + } +} diff --git a/src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs b/src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs new file mode 100644 index 000000000000..de32e0704b41 --- /dev/null +++ b/src/Umbraco.Tests/Persistence/PetaPocoCachesTest.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using NUnit.Framework; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence; +using Umbraco.Core.Services; +using Umbraco.Tests.Services; +using Umbraco.Tests.TestHelpers; +using Umbraco.Tests.TestHelpers.Entities; + +namespace Umbraco.Tests.Persistence +{ + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] + [TestFixture, NUnit.Framework.Ignore] + public class PetaPocoCachesTest : BaseServiceTest + { + +#if DEBUG + /// + /// This tests the peta poco caches + /// + /// + /// This test WILL fail. This is because we cannot stop PetaPoco from creating more cached items for queries such as + /// ContentTypeRepository.GetAll(1,2,3,4); + /// when combined with other GetAll queries that pass in an array of Ids, each query generated for different length + /// arrays will produce a unique query which then gets added to the cache. + /// + /// This test confirms this, if you analyze the DIFFERENCE output below you can see why the cached queries grow. + /// + [Test] + public void Check_Peta_Poco_Caches() + { + var result = new List>>(); + + Database.PocoData.UseLongKeys = true; + + for (int i = 0; i < 2; i++) + { + int id1, id2, id3; + string alias; + CreateStuff(out id1, out id2, out id3, out alias); + QueryStuff(id1, id2, id3, alias); + + double totalBytes1; + IEnumerable keys; + Debug.Print(Database.PocoData.PrintDebugCacheReport(out totalBytes1, out keys)); + + result.Add(new Tuple>(totalBytes1, keys.Count(), keys)); + } + + for (int index = 0; index < result.Count; index++) + { + var tuple = result[index]; + Debug.Print("Bytes: {0}, Delegates: {1}", tuple.Item1, tuple.Item2); + if (index != 0) + { + Debug.Print("----------------DIFFERENCE---------------------"); + var diff = tuple.Item3.Except(result[index - 1].Item3); + foreach (var d in diff) + { + Debug.Print(d); + } + } + + } + + var allByteResults = result.Select(x => x.Item1).Distinct(); + var totalKeys = result.Select(x => x.Item2).Distinct(); + + Assert.AreEqual(1, allByteResults.Count()); + Assert.AreEqual(1, totalKeys.Count()); + } + + [Test] + public void Verify_Memory_Expires() + { + Database.PocoData.SlidingExpirationSeconds = 2; + + var managedCache = new Database.ManagedCache(); + + int id1, id2, id3; + string alias; + CreateStuff(out id1, out id2, out id3, out alias); + QueryStuff(id1, id2, id3, alias); + + var count1 = managedCache.GetCache().GetCount(); + Debug.Print("Keys = " + count1); + Assert.Greater(count1, 0); + + Thread.Sleep(10000); + + var count2 = managedCache.GetCache().GetCount(); + Debug.Print("Keys = " + count2); + Assert.Less(count2, count1); + } + + private void QueryStuff(int id1, int id2, int id3, string alias1) + { + var contentService = ServiceContext.ContentService; + + ServiceContext.TagService.GetTagsForEntity(id1); + + ServiceContext.TagService.GetAllContentTags(); + + ServiceContext.TagService.GetTagsForEntity(id2); + + ServiceContext.TagService.GetTagsForEntity(id3); + + contentService.CountDescendants(id3); + + contentService.CountChildren(id3); + + contentService.Count(contentTypeAlias: alias1); + + contentService.Count(); + + contentService.GetById(Guid.NewGuid()); + + contentService.GetByLevel(2); + + contentService.GetChildren(id1); + + contentService.GetDescendants(id2); + + contentService.GetVersions(id3); + + contentService.GetRootContent(); + + contentService.GetContentForExpiration(); + + contentService.GetContentForRelease(); + + contentService.GetContentInRecycleBin(); + + ((ContentService)contentService).GetPublishedDescendants(new Content("Test", -1, new ContentType(-1)) + { + Id = id1, + Path = "-1," + id1 + }); + + contentService.GetByVersion(Guid.NewGuid()); + } + + private void CreateStuff(out int id1, out int id2, out int id3, out string alias) + { + var contentService = ServiceContext.ContentService; + + var ctAlias = "umbTextpage" + Guid.NewGuid().ToString("N"); + alias = ctAlias; + + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); + } + var contentTypeService = ServiceContext.ContentTypeService; + var contentType = MockedContentTypes.CreateSimpleContentType(ctAlias, "test Doc Type"); + contentTypeService.Save(contentType); + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); + } + var parent = contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); + id1 = parent.Id; + + for (int i = 0; i < 20; i++) + { + contentService.CreateContentWithIdentity("Test", parent, ctAlias); + } + IContent current = parent; + for (int i = 0; i < 20; i++) + { + current = contentService.CreateContentWithIdentity("Test", current, ctAlias); + } + contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory" + Guid.NewGuid().ToString("N"), "Mandatory Doc Type", true); + contentType.PropertyGroups.First().PropertyTypes.Add( + new PropertyType("test", DataTypeDatabaseType.Ntext, "tags") + { + DataTypeDefinitionId = 1041 + }); + contentTypeService.Save(contentType); + var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); + content1.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); + contentService.Publish(content1); + id2 = content1.Id; + + var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); + content2.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); + contentService.Publish(content2); + id3 = content2.Id; + + contentService.MoveToRecycleBin(content1); + } +#endif + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs index 0a159947857f..d06d422d8315 100644 --- a/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs +++ b/src/Umbraco.Tests/Persistence/PetaPocoExtensionsTest.cs @@ -1,219 +1,185 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Text.RegularExpressions; -using System.Threading; using NUnit.Framework; using Umbraco.Core; -using Umbraco.Core.Models; +using Umbraco.Core.Logging; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; -using Umbraco.Core.Services; -using Umbraco.Tests.Services; +using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Tests.TestHelpers; -using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Persistence { [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture, NUnit.Framework.Ignore] - public class PetaPocoCachesTest : BaseServiceTest + [TestFixture] + public class PetaPocoExtensionsTest : BaseDatabaseFactoryTest { - /// - /// This tests the peta poco caches - /// - /// - /// This test WILL fail. This is because we cannot stop PetaPoco from creating more cached items for queries such as - /// ContentTypeRepository.GetAll(1,2,3,4); - /// when combined with other GetAll queries that pass in an array of Ids, each query generated for different length - /// arrays will produce a unique query which then gets added to the cache. - /// - /// This test confirms this, if you analyze the DIFFERENCE output below you can see why the cached queries grow. - /// - [Test] - public void Check_Peta_Poco_Caches() + [SetUp] + public override void Initialize() { - var result = new List>>(); - - Database.PocoData.UseLongKeys = true; + base.Initialize(); + } - for (int i = 0; i < 2; i++) - { - int id1, id2, id3; - string alias; - CreateStuff(out id1, out id2, out id3, out alias); - QueryStuff(id1, id2, id3, alias); + [TearDown] + public override void TearDown() + { + base.TearDown(); + } - double totalBytes1; - IEnumerable keys; - Debug.Print(Database.PocoData.PrintDebugCacheReport(out totalBytes1, out keys)); + [Test] + public void Can_Bulk_Insert_One_By_One() + { + // Arrange + var db = DatabaseContext.Database; - result.Add(new Tuple>(totalBytes1, keys.Count(), keys)); + var servers = new List(); + for (var i = 0; i < 1000; i++) + { + servers.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); } - for (int index = 0; index < result.Count; index++) + // Act + using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) { - var tuple = result[index]; - Debug.Print("Bytes: {0}, Delegates: {1}", tuple.Item1, tuple.Item2); - if (index != 0) + using (var tr = db.GetTransaction()) { - Debug.Print("----------------DIFFERENCE---------------------"); - var diff = tuple.Item3.Except(result[index - 1].Item3); - foreach (var d in diff) - { - Debug.Print(d); - } - } - + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert:false); + tr.Complete(); + } } - var allByteResults = result.Select(x => x.Item1).Distinct(); - var totalKeys = result.Select(x => x.Item2).Distinct(); - - Assert.AreEqual(1, allByteResults.Count()); - Assert.AreEqual(1, totalKeys.Count()); + // Assert + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); } [Test] - public void Verify_Memory_Expires() + public void Can_Bulk_Insert_One_By_One_Transaction_Rollback() { - Database.PocoData.SlidingExpirationSeconds = 2; - - var managedCache = new Database.ManagedCache(); + // Arrange + var db = DatabaseContext.Database; - int id1, id2, id3; - string alias; - CreateStuff(out id1, out id2, out id3, out alias); - QueryStuff(id1, id2, id3, alias); + var servers = new List(); + for (var i = 0; i < 1000; i++) + { + servers.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } - var count1 = managedCache.GetCache().GetCount(); - Debug.Print("Keys = " + count1); - Assert.Greater(count1, 0); - - Thread.Sleep(10000); + // Act + using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) + { + using (var tr = db.GetTransaction()) + { + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert: false); + //don't call complete here - the trans will be rolled back + } + } - var count2 = managedCache.GetCache().GetCount(); - Debug.Print("Keys = " + count2); - Assert.Less(count2, count1); + // Assert + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(0)); } - private void QueryStuff(int id1, int id2, int id3, string alias1) + + [NUnit.Framework.Ignore("Ignored because you need to configure your own SQL Server to test thsi with")] + [Test] + public void Can_Bulk_Insert_Native_Sql_Server_Bulk_Inserts() { - var contentService = ServiceContext.ContentService; - - ServiceContext.TagService.GetTagsForEntity(id1); - - ServiceContext.TagService.GetAllContentTags(); - - ServiceContext.TagService.GetTagsForEntity(id2); - - ServiceContext.TagService.GetTagsForEntity(id3); - - contentService.CountDescendants(id3); - - contentService.CountChildren(id3); - - contentService.Count(contentTypeAlias: alias1); - - contentService.Count(); - - contentService.GetById(Guid.NewGuid()); - - contentService.GetByLevel(2); - - contentService.GetChildren(id1); - - contentService.GetDescendants(id2); - - contentService.GetVersions(id3); - - contentService.GetRootContent(); - - contentService.GetContentForExpiration(); - - contentService.GetContentForRelease(); - - contentService.GetContentInRecycleBin(); + //create the db + var dbSqlServer = new UmbracoDatabase( + "server=.\\SQLExpress;database=YOURDB;user id=YOURUSER;password=YOURPASSWORD", + Constants.DatabaseProviders.SqlServer, + new DebugDiagnosticsLogger()); + + //drop the table + dbSqlServer.Execute("DROP TABLE [umbracoServer]"); + + //re-create it + dbSqlServer.Execute(@"CREATE TABLE [umbracoServer]( + [id] [int] IDENTITY(1,1) NOT NULL, + [address] [nvarchar](500) NOT NULL, + [computerName] [nvarchar](255) NOT NULL, + [registeredDate] [datetime] NOT NULL CONSTRAINT [DF_umbracoServer_registeredDate] DEFAULT (getdate()), + [lastNotifiedDate] [datetime] NOT NULL, + [isActive] [bit] NOT NULL, + [isMaster] [bit] NOT NULL, + CONSTRAINT [PK_umbracoServer] PRIMARY KEY CLUSTERED +( + [id] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +)"); + var data = new List(); + for (var i = 0; i < 1000; i++) + { + data.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); + } - ((ContentService)contentService).GetPublishedDescendants(new Content("Test", -1, new ContentType(-1)) + var sqlServerSyntax = new SqlServerSyntaxProvider(); + using (var tr = dbSqlServer.GetTransaction()) { - Id = id1, - Path = "-1," + id1 - }); + dbSqlServer.BulkInsertRecords(data, tr, sqlServerSyntax, useNativeSqlPlatformBulkInsert: true); + tr.Complete(); + } - contentService.GetByVersion(Guid.NewGuid()); + // Assert + Assert.That(dbSqlServer.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); } - private void CreateStuff(out int id1, out int id2, out int id3, out string alias) + [Test] + public void Can_Bulk_Insert_Native_Sql_Bulk_Inserts() { - var contentService = ServiceContext.ContentService; - - var ctAlias = "umbTextpage" + Guid.NewGuid().ToString("N"); - alias = ctAlias; + // Arrange + var db = DatabaseContext.Database; - for (int i = 0; i < 20; i++) - { - contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); - } - var contentTypeService = ServiceContext.ContentTypeService; - var contentType = MockedContentTypes.CreateSimpleContentType(ctAlias, "test Doc Type"); - contentTypeService.Save(contentType); - for (int i = 0; i < 20; i++) + var servers = new List(); + for (var i = 0; i < 1000; i++) { - contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); + servers.Add(new ServerRegistrationDto + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); } - var parent = contentService.CreateContentWithIdentity("Test", -1, ctAlias, 0); - id1 = parent.Id; - for (int i = 0; i < 20; i++) - { - contentService.CreateContentWithIdentity("Test", parent, ctAlias); - } - IContent current = parent; - for (int i = 0; i < 20; i++) + // Act + using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) { - current = contentService.CreateContentWithIdentity("Test", current, ctAlias); - } - contentType = MockedContentTypes.CreateSimpleContentType("umbMandatory" + Guid.NewGuid().ToString("N"), "Mandatory Doc Type", true); - contentType.PropertyGroups.First().PropertyTypes.Add( - new PropertyType("test", DataTypeDatabaseType.Ntext, "tags") + using (var tr = db.GetTransaction()) { - DataTypeDefinitionId = 1041 - }); - contentTypeService.Save(contentType); - var content1 = MockedContent.CreateSimpleContent(contentType, "Tagged content 1", -1); - content1.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); - contentService.Publish(content1); - id2 = content1.Id; - - var content2 = MockedContent.CreateSimpleContent(contentType, "Tagged content 2", -1); - content2.SetTags("tags", new[] { "hello", "world", "some", "tags" }, true); - contentService.Publish(content2); - id3 = content2.Id; - - contentService.MoveToRecycleBin(content1); - } - } - - [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] - [TestFixture] - public class PetaPocoExtensionsTest : BaseDatabaseFactoryTest - { - [SetUp] - public override void Initialize() - { - base.Initialize(); - } + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert: true); + tr.Complete(); + } + } - [TearDown] - public override void TearDown() - { - base.TearDown(); + // Assert + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); } [Test] - public void Can_Bulk_Insert() + public void Can_Bulk_Insert_Native_Sql_Bulk_Inserts_Transaction_Rollback() { // Arrange var db = DatabaseContext.Database; @@ -222,23 +188,27 @@ public void Can_Bulk_Insert() for (var i = 0; i < 1000; i++) { servers.Add(new ServerRegistrationDto - { - ServerAddress = "address" + i, - ServerIdentity = "computer" + i, - DateRegistered = DateTime.Now, - IsActive = true, - DateAccessed = DateTime.Now - }); + { + ServerAddress = "address" + i, + ServerIdentity = "computer" + i, + DateRegistered = DateTime.Now, + IsActive = true, + DateAccessed = DateTime.Now + }); } // Act using (ProfilingLogger.TraceDuration("starting insert", "finished insert")) { - db.BulkInsertRecords(servers); + using (var tr = db.GetTransaction()) + { + db.BulkInsertRecords(servers, tr, SqlSyntax, useNativeSqlPlatformBulkInsert: true); + //don't call complete here - the trans will be rolled back + } } // Assert - Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(1000)); + Assert.That(db.ExecuteScalar("SELECT COUNT(*) FROM umbracoServer"), Is.EqualTo(0)); } [Test] @@ -263,7 +233,9 @@ public void Generate_Bulk_Import_Sql() // Act string[] sql; - db.GenerateBulkInsertCommand(servers, db.Connection, out sql); + db.GenerateBulkInsertCommand( + Database.PocoData.ForType(typeof(ServerRegistrationDto)), + servers, out sql); db.CloseSharedConnection(); // Assert @@ -295,7 +267,7 @@ public void Generate_Bulk_Import_Sql_Exceeding_Max_Params() // Act string[] sql; - db.GenerateBulkInsertCommand(servers, db.Connection, out sql); + db.GenerateBulkInsertCommand(Database.PocoData.ForType(typeof(ServerRegistrationDto)), servers, out sql); db.CloseSharedConnection(); // Assert diff --git a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs index 4529343811d1..65a660146e55 100644 --- a/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs +++ b/src/Umbraco.Tests/Persistence/Querying/ExpressionTests.cs @@ -16,30 +16,54 @@ namespace Umbraco.Tests.Persistence.Querying [TestFixture] public class ExpressionTests : BaseUsingSqlCeSyntax { - // [Test] - // public void Can_Query_With_Content_Type_Alias() - // { - // //Arrange - // Expression> predicate = content => content.ContentType.Alias == "Test"; - // var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); - // var result = modelToSqlExpressionHelper.Visit(predicate); + // [Test] + // public void Can_Query_With_Content_Type_Alias() + // { + // //Arrange + // Expression> predicate = content => content.ContentType.Alias == "Test"; + // var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); + // var result = modelToSqlExpressionHelper.Visit(predicate); - // Debug.Print("Model to Sql ExpressionHelper: \n" + result); + // Debug.Print("Model to Sql ExpressionHelper: \n" + result); - // Assert.AreEqual("[cmsContentType].[alias] = @0", result); - // Assert.AreEqual("Test", modelToSqlExpressionHelper.GetSqlParameters()[0]); - // } + // Assert.AreEqual("[cmsContentType].[alias] = @0", result); + // Assert.AreEqual("Test", modelToSqlExpressionHelper.GetSqlParameters()[0]); + // } + + [Test] + public void CachedExpression_Can_Verify_Path_StartsWith_Predicate_In_Same_Result() + { + //Arrange + + //use a single cached expression for multiple expressions and ensure the correct output + // is done for both of them. + var cachedExpression = new CachedExpression(); + + + Expression> predicate1 = content => content.Path.StartsWith("-1"); + cachedExpression.Wrap(predicate1); + var modelToSqlExpressionHelper1 = new ModelToSqlExpressionVisitor(); + var result1 = modelToSqlExpressionHelper1.Visit(cachedExpression); + Assert.AreEqual("upper([umbracoNode].[path]) LIKE upper(@0)", result1); + Assert.AreEqual("-1%", modelToSqlExpressionHelper1.GetSqlParameters()[0]); + + Expression> predicate2 = content => content.Path.StartsWith("-1,123,97"); + cachedExpression.Wrap(predicate2); + var modelToSqlExpressionHelper2 = new ModelToSqlExpressionVisitor(); + var result2 = modelToSqlExpressionHelper2.Visit(cachedExpression); + Assert.AreEqual("upper([umbracoNode].[path]) LIKE upper(@0)", result2); + Assert.AreEqual("-1,123,97%", modelToSqlExpressionHelper2.GetSqlParameters()[0]); + + } [Test] public void Can_Verify_Path_StartsWith_Predicate_In_Same_Result() { //Arrange Expression> predicate = content => content.Path.StartsWith("-1"); - var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); - - Debug.Print("Model to Sql ExpressionHelper: \n" + result); - + Assert.AreEqual("upper([umbracoNode].[path]) LIKE upper(@0)", result); Assert.AreEqual("-1%", modelToSqlExpressionHelper.GetSqlParameters()[0]); } @@ -49,7 +73,7 @@ public void Can_Verify_ParentId_StartsWith_Predicate_In_Same_Result() { //Arrange Expression> predicate = content => content.ParentId == -1; - var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); Debug.Print("Model to Sql ExpressionHelper: \n" + result); @@ -62,7 +86,7 @@ public void Can_Verify_ParentId_StartsWith_Predicate_In_Same_Result() public void Equals_Operator_For_Value_Gets_Escaped() { Expression> predicate = user => user.Username == "hello@world.com"; - var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); Debug.Print("Model to Sql ExpressionHelper: \n" + result); @@ -75,7 +99,7 @@ public void Equals_Operator_For_Value_Gets_Escaped() public void Equals_Method_For_Value_Gets_Escaped() { Expression> predicate = user => user.Username.Equals("hello@world.com"); - var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); Debug.Print("Model to Sql ExpressionHelper: \n" + result); @@ -91,7 +115,7 @@ public void Model_Expression_Value_Does_Not_Get_Double_Escaped() SqlSyntaxContext.SqlSyntaxProvider = new MySqlSyntaxProvider(Mock.Of()); Expression> predicate = user => user.Username.Equals("mydomain\\myuser"); - var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); Debug.Print("Model to Sql ExpressionHelper: \n" + result); @@ -108,7 +132,7 @@ public void Poco_Expression_Value_Does_Not_Get_Double_Escaped() SqlSyntaxContext.SqlSyntaxProvider = new MySqlSyntaxProvider(Mock.Of()); Expression> predicate = user => user.Login.StartsWith("mydomain\\myuser"); - var modelToSqlExpressionHelper = new PocoToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new PocoToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); Debug.Print("Poco to Sql ExpressionHelper: \n" + result); @@ -121,7 +145,7 @@ public void Poco_Expression_Value_Does_Not_Get_Double_Escaped() public void Sql_Replace_Mapped() { Expression> predicate = user => user.Username.Replace("@world", "@test") == "hello@test.com"; - var modelToSqlExpressionHelper = new ModelToSqlExpressionHelper(); + var modelToSqlExpressionHelper = new ModelToSqlExpressionVisitor(); var result = modelToSqlExpressionHelper.Visit(predicate); Debug.Print("Model to Sql ExpressionHelper: \n" + result); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index abcb5b3d6a19..311ea52071db 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -101,7 +101,7 @@ public void Rebuild_Xml_Structures_With_Non_Latest_Version() for (int i = 0; i < allCreated.Count; i++) { allCreated[i].Name = "blah" + i; - //IMPORTANT testing note here: We need to changed the published state here so that + //IMPORTANT testing note here: We need to changed the published state here so that // it doesn't automatically think this is simply publishing again - this forces the latest // version to be Saved and not published allCreated[i].ChangePublishedState(PublishedState.Saved); @@ -109,7 +109,7 @@ public void Rebuild_Xml_Structures_With_Non_Latest_Version() } unitOfWork.Commit(); - //delete all xml + //delete all xml unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); @@ -156,7 +156,7 @@ public void Rebuild_All_Xml_Structures() } unitOfWork.Commit(); - //delete all xml + //delete all xml unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); @@ -221,7 +221,7 @@ public void Rebuild_All_Xml_Structures_For_Content_Type() } unitOfWork.Commit(); - //delete all xml + //delete all xml unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); @@ -234,10 +234,10 @@ public void Rebuild_All_Xml_Structures_For_Content_Type() /// /// This test ensures that when property values using special database fields are saved, the actual data in the /// object being stored is also transformed in the same way as the data being stored in the database is. - /// Before you would see that ex: a decimal value being saved as 100 or "100", would be that exact value in the + /// Before you would see that ex: a decimal value being saved as 100 or "100", would be that exact value in the /// object, but the value saved to the database was actually 100.000000. - /// When querying the database for the value again - the value would then differ from what is in the object. - /// This caused inconsistencies between saving+publishing and simply saving and then publishing, due to the former + /// When querying the database for the value again - the value would then differ from what is in the object. + /// This caused inconsistencies between saving+publishing and simply saving and then publishing, due to the former /// sending the non-transformed data directly on to publishing. /// [Test] @@ -269,10 +269,10 @@ public void Property_Values_With_Special_DatabaseTypes_Are_Equal_Before_And_Afte var contentType = MockedContentTypes.CreateSimpleContentType("umbTextpage1", "Textpage", propertyTypeCollection); contentTypeRepository.AddOrUpdate(contentType); unitOfWork.Commit(); - + // Int and decimal values are passed in as strings as they would be from the backoffice UI var textpage = MockedContent.CreateSimpleContentWithSpecialDatabaseTypes(contentType, "test@umbraco.org", -1, "100", "150", dateValue); - + // Act repository.AddOrUpdate(textpage); unitOfWork.Commit(); @@ -280,7 +280,7 @@ public void Property_Values_With_Special_DatabaseTypes_Are_Equal_Before_And_Afte // Assert Assert.That(contentType.HasIdentity, Is.True); Assert.That(textpage.HasIdentity, Is.True); - + var persistedTextpage = repository.Get(textpage.Id); Assert.That(persistedTextpage.Name, Is.EqualTo(textpage.Name)); Assert.AreEqual(100m, persistedTextpage.GetValue(decimalPropertyAlias)); @@ -342,12 +342,12 @@ public void Can_Perform_Add_On_ContentRepository() contentTypeRepository.AddOrUpdate(contentType); repository.AddOrUpdate(textpage); unitOfWork.Commit(); - + // Assert Assert.That(contentType.HasIdentity, Is.True); Assert.That(textpage.HasIdentity, Is.True); - + } } @@ -631,9 +631,9 @@ public void Can_Perform_GetPagedResultsByQuery_Sorting_On_Custom_Property() // Act var query = Query.Builder.Where(x => x.Name.Contains("Text")); long totalRecords; - + try - { + { DatabaseContext.Database.EnableSqlTrace = true; DatabaseContext.Database.EnableSqlCount(); @@ -643,14 +643,14 @@ public void Can_Perform_GetPagedResultsByQuery_Sorting_On_Custom_Property() Assert.AreEqual(2, result.Count()); result = repository.GetPagedResultsByQuery(query, 1, 2, out totalRecords, "title", Direction.Ascending, false); - + Assert.AreEqual(1, result.Count()); } finally - { + { DatabaseContext.Database.EnableSqlTrace = false; DatabaseContext.Database.DisableSqlCount(); - } + } } } @@ -761,7 +761,7 @@ public void Can_Perform_GetPagedResultsByQuery_WithFilterMatchingSome_On_Content // Act var query = Query.Builder.Where(x => x.Level == 2); var filterQuery = Query.Builder.Where(x => x.Name.Contains("Page 2")); - + long totalRecords; var result = repository.GetPagedResultsByQuery(query, 0, 1, out totalRecords, "Name", Direction.Ascending, true, filterQuery); diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 7d8d7659c8ad..84cdef73e149 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -539,7 +539,7 @@ public void Can_Perform_Query_On_ContentTypeRepository_Sort_By_Name() using (var repository = CreateRepository(unitOfWork)) { var contentType = repository.Get(NodeDto.NodeIdSeed + 1); - var child1 = MockedContentTypes.CreateSimpleContentType("aabc", "aabc", contentType, randomizeAliases: true); + var child1 = MockedContentTypes.CreateSimpleContentType("abc", "abc", contentType, randomizeAliases: true); repository.AddOrUpdate(child1); var child3 = MockedContentTypes.CreateSimpleContentType("zyx", "zyx", contentType, randomizeAliases: true); repository.AddOrUpdate(child3); @@ -553,7 +553,7 @@ public void Can_Perform_Query_On_ContentTypeRepository_Sort_By_Name() // Assert Assert.That(contentTypes.Count(), Is.EqualTo(3)); Assert.AreEqual("a123", contentTypes.ElementAt(0).Name); - Assert.AreEqual("aabc", contentTypes.ElementAt(1).Name); + Assert.AreEqual("abc", contentTypes.ElementAt(1).Name); Assert.AreEqual("zyx", contentTypes.ElementAt(2).Name); } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index a216eaaa45bc..1566441d5a40 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Moq; @@ -59,7 +60,7 @@ public void Rebuild_All_Xml_Structures() } unitOfWork.Commit(); - //delete all xml + //delete all xml unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); @@ -69,6 +70,39 @@ public void Rebuild_All_Xml_Structures() } } + [Test] + public void Rebuild_Some_Xml_Structures() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + MediaTypeRepository mediaTypeRepository; + using (var repository = CreateRepository(unitOfWork, out mediaTypeRepository)) + { + + var mediaType = mediaTypeRepository.Get(1032); + + IMedia img50 = null; + for (var i = 0; i < 100; i++) + { + var image = MockedMedia.CreateMediaImage(mediaType, -1); + repository.AddOrUpdate(image); + if (i == 50) img50 = image; + } + unitOfWork.Commit(); + + // assume this works (see other test) + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + //delete some xml + unitOfWork.Database.Execute("DELETE FROM cmsContentXml WHERE nodeId < " + img50.Id); + Assert.AreEqual(50, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + + repository.RebuildXmlStructures(media => new XElement("test"), 10); + + Assert.AreEqual(103, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); + } + } + [Test] public void Rebuild_All_Xml_Structures_For_Content_Type() { @@ -99,7 +133,7 @@ public void Rebuild_All_Xml_Structures_For_Content_Type() } unitOfWork.Commit(); - //delete all xml + //delete all xml unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index bb13f055c976..e97fff03c89a 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -54,7 +54,7 @@ public void Rebuild_All_Xml_Structures() using (var repository = CreateRepository(unitOfWork, out memberTypeRepository, out memberGroupRepository)) { var memberType1 = CreateTestMemberType(); - + for (var i = 0; i < 100; i++) { var member = MockedMember.CreateSimpleMember(memberType1, "blah" + i, "blah" + i + "@example.com", "blah", "blah" + i); @@ -103,7 +103,7 @@ public void Rebuild_All_Xml_Structures_For_Content_Type() } unitOfWork.Commit(); - //delete all xml + //delete all xml unitOfWork.Database.Execute("DELETE FROM cmsContentXml"); Assert.AreEqual(0, unitOfWork.Database.ExecuteScalar("SELECT COUNT(*) FROM cmsContentXml")); @@ -216,7 +216,7 @@ public void Can_Persist_Member() var sut = repository.Get(member.Id); Assert.That(sut, Is.Not.Null); - Assert.That(sut.HasIdentity, Is.True); + Assert.That(sut.HasIdentity, Is.True); Assert.That(sut.Properties.Any(x => x.HasIdentity == false || x.Id == 0), Is.False); Assert.That(sut.Name, Is.EqualTo("Johnny Hefty")); Assert.That(sut.Email, Is.EqualTo("johnny@example.com")); @@ -350,7 +350,7 @@ private IMember CreateTestMember(IMemberType memberType = null, string name = nu { memberType = MockedContentTypes.CreateSimpleMemberType(); memberTypeRepository.AddOrUpdate(memberType); - unitOfWork.Commit(); + unitOfWork.Commit(); } var member = MockedMember.CreateSimpleMember(memberType, name ?? "Johnny Hefty", email ?? "johnny@example.com", password ?? "123", username ?? "hefty", key); diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs index 8126aa5e363a..e58506aa035c 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/MySqlSyntaxProviderTests.cs @@ -28,7 +28,7 @@ public void SetUp() public void Can_Generate_Create_Table_Statement() { var type = typeof(TagRelationshipDto); - var definition = DefinitionFactory.GetTableDefinition(type); + var definition = DefinitionFactory.GetTableDefinition(SqlSyntaxContext.SqlSyntaxProvider, type); string create = SqlSyntaxContext.SqlSyntaxProvider.Format(definition); string primaryKey = SqlSyntaxContext.SqlSyntaxProvider.FormatPrimaryKey(definition); diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index e960f50799ab..fafddb8dfd64 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -51,7 +51,7 @@ public void Can_Generate_Create_Table_Statement() var sqlSyntax = new SqlCeSyntaxProvider(); var type = typeof (NodeDto); - var definition = DefinitionFactory.GetTableDefinition(type); + var definition = DefinitionFactory.GetTableDefinition(sqlSyntax, type); string create = sqlSyntax.Format(definition); string primaryKey = sqlSyntax.FormatPrimaryKey(definition); diff --git a/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs index 8a83bea75a5c..5bf6a4edc5af 100644 --- a/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/LegacyExamineBackedMediaTests.cs @@ -13,6 +13,7 @@ namespace Umbraco.Tests.PublishedContent { + [DatabaseTestBehavior(DatabaseBehavior.NewDbFileAndSchemaPerTest)] public class LegacyExamineBackedMediaTests : ExamineBaseTest { public override void Initialize() diff --git a/src/Umbraco.Tests/Resolvers/ManyResolverTests.cs b/src/Umbraco.Tests/Resolvers/ManyResolverTests.cs index 4cf1c3b56ffa..3ec470de4b7e 100644 --- a/src/Umbraco.Tests/Resolvers/ManyResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/ManyResolverTests.cs @@ -36,7 +36,7 @@ public abstract class Resolved public class Resolved1 : Resolved { } - [WeightedPlugin(5)] // default is 10 + [Weight(5)] // default is 100 public class Resolved2 : Resolved { } diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 70188eaa9de2..ed4a59bab45a 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -68,6 +68,50 @@ public void Remove_Scheduled_Publishing_Date() Assert.IsTrue(contentService.PublishWithStatus(content).Success); } + [Test] + public void Get_Top_Version_Ids() + { + // Arrange + var contentService = ServiceContext.ContentService; + + // Act + var content = contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0); + for (int i = 0; i < 20; i++) + { + content.SetValue("bodyText", "hello world " + Guid.NewGuid()); + contentService.SaveAndPublishWithStatus(content); + } + + + // Assert + var allVersions = contentService.GetVersionIds(content.Id, int.MaxValue); + Assert.AreEqual(21, allVersions.Count()); + + var topVersions = contentService.GetVersionIds(content.Id, 4); + Assert.AreEqual(4, topVersions.Count()); + } + + [Test] + public void Get_By_Ids_Sorted() + { + // Arrange + var contentService = ServiceContext.ContentService; + + // Act + var results = new List(); + for (int i = 0; i < 20; i++) + { + results.Add(contentService.CreateContentWithIdentity("Test", -1, "umbTextpage", 0)); + } + + var sortedGet = contentService.GetByIds(new[] {results[10].Id, results[5].Id, results[12].Id}).ToArray(); + + // Assert + Assert.AreEqual(sortedGet[0].Id, results[10].Id); + Assert.AreEqual(sortedGet[1].Id, results[5].Id); + Assert.AreEqual(sortedGet[2].Id, results[12].Id); + } + [Test] public void Count_All() { diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 5e362ee1f867..6c8fc9308dca 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -142,7 +142,13 @@ public static void AssertAllPropertyValuesAreEquals(object actual, object expect } else if (dateTimeFormat.IsNullOrWhiteSpace() == false && actualValue is DateTime) { - Assert.AreEqual(((DateTime) expectedValue).ToString(dateTimeFormat), ((DateTime)actualValue).ToString(dateTimeFormat), "Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue); + // round to second else in some cases tests can fail ;-( + var expectedDateTime = (DateTime) expectedValue; + expectedDateTime = expectedDateTime.AddTicks(-(expectedDateTime.Ticks%TimeSpan.TicksPerSecond)); + var actualDateTime = (DateTime) actualValue; + actualDateTime = actualDateTime.AddTicks(-(actualDateTime.Ticks % TimeSpan.TicksPerSecond)); + + Assert.AreEqual(expectedDateTime.ToString(dateTimeFormat), actualDateTime.ToString(dateTimeFormat), "Property {0}.{1} does not match. Expected: {2} but was: {3}", property.DeclaringType.Name, property.Name, expectedValue, actualValue); } else { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index c2c6e785ec71..a606d401a0a7 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -51,16 +51,16 @@ false - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll True - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\Examine.0.1.69.0\lib\Examine.dll + + ..\packages\Examine.0.1.70.0\lib\Examine.dll True @@ -176,7 +176,9 @@ + + @@ -765,6 +767,7 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" /Y /F /E /I /C /D + - {{item.name}} + {{item.name}} - {{item.name}} + {{item.name}} {{item.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index 6a05d234f064..6b05593e1135 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -98,4 +98,11 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 3f2bfcdd3bbe..371e15651306 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -51,13 +51,14 @@
{{error.errorMsg}}
- - + + + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index b64511ec221f..6bf3ca817a6b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -3,9 +3,11 @@

- Choose where to move {{currentNode.name}} to in the tree structure below + Choose where to move + {{currentNode.name}} + to in the tree structure below

- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js index 53c0e0419db0..fb0c736f7b13 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/dashboard.tabs.controller.js @@ -14,9 +14,42 @@ function startUpVideosDashboardController($scope, xmlhelper, $log, $http) { }); }; } + angular.module("umbraco").controller("Umbraco.Dashboard.StartupVideosController", startUpVideosDashboardController); +function startUpDynamicContentController(dashboardResource, assetsService) { + var vm = this; + vm.loading = true; + vm.showDefault = false; + + //proxy remote css through the local server + assetsService.loadCss( dashboardResource.getRemoteDashboardCssUrl("content") ); + dashboardResource.getRemoteDashboardContent("content").then( + function (data) { + + vm.loading = false; + + //test if we have received valid data + //we capture it like this, so we avoid UI errors - which automatically triggers ui based on http response code + if (data && data.sections) { + vm.dashboard = data; + } else{ + vm.showDefault = true; + } + + }, + + function (exception) { + console.error(exception); + vm.loading = false; + vm.showDefault = true; + }); +} + +angular.module("umbraco").controller("Umbraco.Dashboard.StartUpDynamicContentController", startUpDynamicContentController); + + function FormsController($scope, $route, $cookieStore, packageResource, localizationService) { $scope.installForms = function(){ $scope.state = localizationService.localize("packager_installStateDownloading"); @@ -192,27 +225,43 @@ function startupLatestEditsController($scope) { } angular.module("umbraco").controller("Umbraco.Dashboard.StartupLatestEditsController", startupLatestEditsController); -function MediaFolderBrowserDashboardController($rootScope, $scope, contentTypeResource) { - - //get the system media listview - contentTypeResource.getPropertyTypeScaffold(-96) - .then(function(dt) { - - $scope.fakeProperty = { - alias: "contents", - config: dt.config, - description: "", - editor: dt.editor, - hideLabel: true, - id: 1, - label: "Contents:", - validation: { - mandatory: false, - pattern: null - }, - value: "", - view: dt.view - }; +function MediaFolderBrowserDashboardController($rootScope, $scope, $location, contentTypeResource, userService) { + + var currentUser = {}; + + userService.getCurrentUser().then(function (user) { + + currentUser = user; + + // check if the user start node is the dashboard + if(currentUser.startMediaId === -1) { + + //get the system media listview + contentTypeResource.getPropertyTypeScaffold(-96) + .then(function(dt) { + + $scope.fakeProperty = { + alias: "contents", + config: dt.config, + description: "", + editor: dt.editor, + hideLabel: true, + id: 1, + label: "Contents:", + validation: { + mandatory: false, + pattern: null + }, + value: "", + view: dt.view + }; + + }); + + } else { + // redirect to start node + $location.path("/media/media/edit/" + currentUser.startMediaId); + } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html index ff933a415ed9..37828983126f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/default/StartupDashboardIntro.html @@ -1,44 +1,95 @@ - -
-

Welcome to The Friendly CMS

-

Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible:

- -
-
- - Umbraco.TV - Hours of Umbraco Video Tutorials - - - - -

Umbraco.TV - Learn from the source!

-
- -

- Umbraco.TV will help you go from zero to Umbraco - hero at a pace that suits you. Our easy to follow - online training videos will give you the fundamental - knowledge to start building awesome Umbraco websites. -

-
- -
- - - Our Umbraco - - - -

Our Umbraco - The Friendliest Community

-
- -

- Our Umbraco - the official community site is your one - stop for everything Umbraco. Whether you need a - question answered or looking for cool plugins, the - worlds best community is just a click away. -

-
-
- -
+
+ + + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+ +
+
+ +
+
+
+ + +
+
+ + + + +
+

Welcome to The Friendly CMS

+

+ Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. +

+

Find out more:

+ + +
+
+ + Umbraco.TV - Hours of Umbraco Video Tutorials + + + + +

Umbraco.TV - Learn from the source!

+
+ +

+ Umbraco.TV will help you go from zero to Umbraco + hero at a pace that suits you. Our easy to follow + online training videos will give you the fundamental + knowledge to start building awesome Umbraco websites. +

+
+ +
+ + + Our Umbraco + + + +

Our Umbraco - The Friendliest Community

+
+ +

+ Our Umbraco - the official community site is your one + stop for everything Umbraco. Whether you need a + question answered or looking for cool plugins, the + worlds best community is just a click away. +

+ +
+
+
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html index c63a44483957..f23b8f5df98b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/developer/redirecturls.html @@ -77,7 +77,7 @@ {{redirectUrl.destinationUrl}}
- + diff --git a/src/Umbraco.Web.UI.Client/src/views/media/move.html b/src/Umbraco.Web.UI.Client/src/views/media/move.html index 95c30dfc4022..3f71340ee38d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/move.html @@ -3,8 +3,10 @@

- Choose where to move {{currentNode.name}} to in the tree structure below -

+ Choose where to move + {{currentNode.name}} + to in the tree structure below +

{{error.errorMsg}}

diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js index e29df0b9ae11..e34bc48ecdaf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.controller.js @@ -12,6 +12,7 @@ status: "", progress:0 }; + vm.installCompleted = false; vm.zipFile = { uploadStatus: "idle", uploadProgress: 0, @@ -34,6 +35,9 @@ file: file }).progress(function (evt) { + // set view state to uploading + vm.state = 'uploading'; + // calculate progress in percentage var progressPercentage = parseInt(100.0 * evt.loaded / evt.total, 10); @@ -134,10 +138,10 @@ localStorageService.set("packageInstallUri", "installed"); } - //reload on next digest (after cookie) - $timeout(function () { - $window.location.reload(true); - }); + vm.installState.status = localizationService.localize("packager_installStateCompleted"); + vm.installCompleted = true; + + }, installError); @@ -147,6 +151,13 @@ //This will return a rejection meaning that the promise change above will stop return $q.reject(); } + + vm.reloadPage = function() { + //reload on next digest (after cookie) + $timeout(function () { + $window.location.reload(true); + }); + } } angular.module("umbraco").controller("Umbraco.Editors.Packages.InstallLocalController", PackagesInstallLocalController); diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html index 07fb21d00b56..6bb7c6fb1409 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/install-local.html @@ -37,10 +37,8 @@ - or click here to choose files
-
- {{vm.zipFile.serverErrorMessage}} -
+ @@ -53,6 +51,41 @@

Upload package

+
+ + + + ← Upload another package + + + +
+ +
+
+
+ +
+
+ +

Uploading package

+ + + + +
+ {{ vm.zipFile.serverErrorMessage }} +
+ +
+
+
+ +
+ +
+
@@ -126,6 +159,19 @@

{{ vm.localPackage.name }}

{{vm.installState.status}}

+ + +
+ + +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js index e4afb661e3b2..5ae1d4bf2c9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.controller.js @@ -30,6 +30,7 @@ vm.openLightbox = openLightbox; vm.closeLightbox = closeLightbox; vm.search = search; + vm.installCompleted = false; var currSort = "Latest"; //used to cancel any request in progress if another one needs to take it's place @@ -215,10 +216,8 @@ localStorageService.set("packageInstallUri", result.postInstallationPath); } - //reload on next digest (after cookie) - $timeout(function() { - window.location.reload(true); - }); + vm.installState.status = localizationService.localize("packager_installStateCompleted"); + vm.installCompleted = true; }, error); @@ -277,6 +276,13 @@ searchDebounced(); } + vm.reloadPage = function () { + //reload on next digest (after cookie) + $timeout(function () { + window.location.reload(true); + }); + } + init(); } diff --git a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html index b19eb63232a4..e7b14182caa5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packager/views/repo.html @@ -199,7 +199,7 @@

We couldn't find anything for '{{ vm.searchQuery }}'

Created:
-
{{vm.package.created}}
+
{{vm.package.created | date:'yyyy-MM-dd HH:mm:ss'}}
@@ -340,6 +340,16 @@

{{ vm.localPackage.name }}

{{vm.installState.status}}

+
+ + +
+ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js index e886cb977e6f..e2422e7da0ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.controller.js @@ -16,7 +16,7 @@ angular.module("umbraco") }; $scope.scaleDown = function(section){ - var remove = (section.grid > 1) ? 1 : section.grid; + var remove = (section.grid > 1) ? 1 : 0; section.grid = section.grid-remove; }; @@ -49,9 +49,12 @@ angular.module("umbraco") $scope.currentSection = section; }; - - $scope.deleteSection = function(index){ - $scope.currentTemplate.sections.splice(index, 1); + $scope.deleteSection = function(section, template) { + if ($scope.currentSection === section) { + $scope.currentSection = undefined; + } + var index = template.sections.indexOf(section) + template.sections.splice(index, 1); }; $scope.closeSection = function(){ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html index 9cbe74bae295..f87262daaf74 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html @@ -35,17 +35,24 @@
-
+
- + {{currentSection.grid}} - +
+ + + + Delete + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js index 09fa2633549f..3cd16301cd62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.controller.js @@ -14,7 +14,7 @@ function RowConfigController($scope) { }; $scope.scaleDown = function(section) { - var remove = (section.grid > 1) ? 1 : section.grid; + var remove = (section.grid > 1) ? 1 : 0; section.grid = section.grid - remove; }; @@ -58,9 +58,14 @@ function RowConfigController($scope) { } }; - $scope.deleteArea = function(index) { - $scope.currentRow.areas.splice(index, 1); + $scope.deleteArea = function (cell, row) { + if ($scope.currentCell === cell) { + $scope.currentCell = undefined; + } + var index = row.areas.indexOf(cell) + row.areas.splice(index, 1); }; + $scope.closeArea = function() { $scope.currentCell = undefined; }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index f47a1958ae21..421aaf0d4044 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -44,19 +44,23 @@
    -
    -
    - - - +
    + + + {{currentCell.grid}} - - - -
    -
    + + + +
    + + + + Delete + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index e274249b42e9..e1c011849707 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -44,7 +44,7 @@ center="model.value.focalPoint" on-image-loaded="imageLoaded"> - Remove file + Remove file
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html index 4360996b558c..16c5efe799cb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts/grid/grid.html @@ -39,7 +39,8 @@ on-drag-enter="vm.dragEnter()"> - + - + - + {{image.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js index a1e48bbc99c2..f965b812d815 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.controller.js @@ -1,6 +1,6 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.TagsController", - function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element) { + function ($rootScope, $scope, $log, assetsService, umbRequestHelper, angularHelper, $timeout, $element, $sanitize) { var $typeahead; @@ -41,6 +41,7 @@ angular.module("umbraco") //Helper method to add a tag on enter or on typeahead select function addTag(tagToAdd) { + tagToAdd = String(tagToAdd).htmlEncode(); if (tagToAdd != null && tagToAdd.length > 0) { if ($scope.model.value.indexOf(tagToAdd) < 0) { $scope.model.value.push(tagToAdd); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 4e9774e18b0b..c8c90c2ed54c 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -107,16 +107,16 @@ {07fbc26b-2927-4a22-8d96-d644c667fecc} UmbracoExamine - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll True - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\ClientDependency.1.9.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll True @@ -127,20 +127,20 @@ False ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - - ..\packages\Examine.0.1.69.0\lib\Examine.dll + + ..\packages\Examine.0.1.70.0\lib\Examine.dll True False ..\packages\SharpZipLib.0.86.0\lib\20\ICSharpCode.SharpZipLib.dll - - ..\packages\ImageProcessor.2.4.4.0\lib\net45\ImageProcessor.dll + + ..\packages\ImageProcessor.2.4.5.0\lib\net45\ImageProcessor.dll True - - ..\packages\ImageProcessor.Web.4.6.4.0\lib\net45\ImageProcessor.Web.dll + + ..\packages\ImageProcessor.Web.4.6.6.0\lib\net45\ImageProcessor.Web.dll True @@ -347,8 +347,8 @@ umbraco.providers - - ..\packages\Umbraco.ModelsBuilder.3.0.4\lib\Umbraco.ModelsBuilder.dll + + ..\packages\Umbraco.ModelsBuilder.3.0.5\lib\Umbraco.ModelsBuilder.dll True @@ -1973,6 +1973,8 @@ + + Web.Template.config @@ -2396,6 +2398,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 @@ -2412,9 +2415,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" True True - 7530 + 7550 / - http://localhost:7530 + http://localhost:7550 False False @@ -2453,4 +2456,5 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.1\x86\*.* "$(TargetDir)x86\" + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2-Fluid.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2-Fluid.cshtml index 446a82f510b3..f6b93139ce46 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2-Fluid.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2-Fluid.cshtml @@ -64,21 +64,29 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); + foreach (JProperty property in cfg.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + attrs.Add(property.Name + "=\"" + propertyValue + "\""); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + if (string.IsNullOrWhiteSpace(propertyValue) == false) + { + cssVals.Add(property.Name + ":" + propertyValue + ";"); + } + } - if (cssVals.Any()) - attrs.Add("style='" + string.Join(" ", cssVals) + "'"); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml index 6bc730e1f8ea..c5fabe2abf2d 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap2.cshtml @@ -64,21 +64,29 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "=\"" + property.Value.ToString() + "\""); + foreach (JProperty property in cfg.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + attrs.Add(property.Name + "=\"" + propertyValue + "\""); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + if (string.IsNullOrWhiteSpace(propertyValue) == false) + { + cssVals.Add(property.Name + ":" + propertyValue + ";"); + } + } - if (cssVals.Any()) - attrs.Add("style=\"" + string.Join(" ", cssVals) + "\""); + if (cssVals.Any()) + attrs.Add("style=\"" + string.Join(" ", cssVals) + "\""); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml index 1244821d7e67..b7e8ef34fb09 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3-Fluid.cshtml @@ -5,6 +5,7 @@ @* Razor helpers located at the bottom of this file *@ + @if (Model != null && Model.sections != null) { var oneColumn = ((System.Collections.ICollection)Model.sections).Count == 1; @@ -59,21 +60,29 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "='" + property.Value.ToString() + "'"); + foreach (JProperty property in cfg.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + attrs.Add(property.Name + "=\"" + propertyValue + "\""); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + if (string.IsNullOrWhiteSpace(propertyValue) == false) + { + cssVals.Add(property.Name + ":" + propertyValue + ";"); + } + } - if (cssVals.Any()) - attrs.Add("style='" + string.Join(" ", cssVals) + "'"); + if (cssVals.Any()) + attrs.Add("style='" + string.Join(" ", cssVals) + "'"); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml index f76028d29680..3a4fa3b8e2eb 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Bootstrap3.cshtml @@ -64,21 +64,29 @@ JObject cfg = contentItem.config; if(cfg != null) - foreach (JProperty property in cfg.Properties()) { - attrs.Add(property.Name + "=\"" + property.Value.ToString() + "\""); + foreach (JProperty property in cfg.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + attrs.Add(property.Name + "=\"" + propertyValue + "\""); } - + JObject style = contentItem.styles; - if (style != null) { - var cssVals = new List(); - foreach (JProperty property in style.Properties()) - cssVals.Add(property.Name + ":" + property.Value.ToString() + ";"); + if (style != null) { + var cssVals = new List(); + foreach (JProperty property in style.Properties()) + { + var propertyValue = TemplateUtilities.CleanForXss(property.Value.ToString()); + if (string.IsNullOrWhiteSpace(propertyValue) == false) + { + cssVals.Add(property.Name + ":" + propertyValue + ";"); + } + } - if (cssVals.Any()) - attrs.Add("style=\"" + string.Join(" ", cssVals) + "\""); + if (cssVals.Any()) + attrs.Add("style=\"" + string.Join(" ", cssVals) + "\""); } - + return new MvcHtmlString(string.Join(" ", attrs)); } } \ No newline at end of file diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml index a86c04819a1f..ffb7603048b7 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Base.cshtml @@ -1,5 +1,4 @@ @model dynamic -@using Umbraco.Web.Templates @functions { public static string EditorView(dynamic contentItem) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml index 4fd66ddb901b..c27be6bcdf68 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Embed.cshtml @@ -1,3 +1,2 @@ @model dynamic -@using Umbraco.Web.Templates @Html.Raw(Model.value) diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml index e0822808d8a2..ed08bb2484fe 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Macro.cshtml @@ -1,6 +1,4 @@ @inherits UmbracoViewPage -@using Umbraco.Web.Templates - @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml index f5dfc6459c2f..5b5adbdc7d30 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/Media.cshtml @@ -1,5 +1,4 @@ @model dynamic -@using Umbraco.Web.Templates @if (Model.value != null) { diff --git a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml index a031c658a9ca..5a570efdb55f 100644 --- a/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/Grid/Editors/TextString.cshtml @@ -4,10 +4,9 @@ @if (Model.editor.config.markup != null) { string markup = Model.editor.config.markup.ToString(); - var UmbracoHelper = new UmbracoHelper(UmbracoContext.Current); - markup = markup.Replace("#value#", UmbracoHelper.ReplaceLineBreaksForHtml(Model.value.ToString())); + markup = markup.Replace("#value#", UmbracoHelper.ReplaceLineBreaksForHtml(TemplateUtilities.CleanForXss(Model.value.ToString()))); markup = markup.Replace("#style#", Model.editor.config.style.ToString()); diff --git a/src/Umbraco.Web.UI/config/Dashboard.config b/src/Umbraco.Web.UI/config/Dashboard.config index 81bd9be6e887..c40218c8d257 100644 --- a/src/Umbraco.Web.UI/config/Dashboard.config +++ b/src/Umbraco.Web.UI/config/Dashboard.config @@ -99,23 +99,23 @@ -
      +
      - developer + content - + - /App_Plugins/ModelsBuilder/modelsbuilder.htm + views/dashboard/developer/redirecturls.html
      -
      +
      - content + developer - + - views/dashboard/developer/redirecturls.html + /App_Plugins/ModelsBuilder/modelsbuilder.htm
      diff --git a/src/Umbraco.Web.UI/config/UrlRewriting.Release.config b/src/Umbraco.Web.UI/config/UrlRewriting.Release.config index ce7076bf9681..754643cf306c 100644 --- a/src/Umbraco.Web.UI/config/UrlRewriting.Release.config +++ b/src/Umbraco.Web.UI/config/UrlRewriting.Release.config @@ -2,32 +2,9 @@ + URLRewriting.net is obsolete and will be removed from Umbraco in the future. + If you want to do rewrites, make sure to use IIS URL rewrite: https://www.iis.net/downloads/microsoft/url-rewrite + The advantage of using IIS rewrite is that it is much faster, much less CPU intensive and much less memory intensive. + --> diff --git a/src/Umbraco.Web.UI/config/UrlRewriting.config b/src/Umbraco.Web.UI/config/UrlRewriting.config index 108f53bf5e76..754643cf306c 100644 --- a/src/Umbraco.Web.UI/config/UrlRewriting.config +++ b/src/Umbraco.Web.UI/config/UrlRewriting.config @@ -2,32 +2,9 @@ + URLRewriting.net is obsolete and will be removed from Umbraco in the future. + If you want to do rewrites, make sure to use IIS URL rewrite: https://www.iis.net/downloads/microsoft/url-rewrite + The advantage of using IIS rewrite is that it is much faster, much less CPU intensive and much less memory intensive. + --> - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI/config/grid.editors.config.js b/src/Umbraco.Web.UI/config/grid.editors.config.js index 7686de527099..b90492056672 100644 --- a/src/Umbraco.Web.UI/config/grid.editors.config.js +++ b/src/Umbraco.Web.UI/config/grid.editors.config.js @@ -39,7 +39,7 @@ "view": "textstring", "icon": "icon-quote", "config": { - "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-variant: italic; font-size: 18px", + "style": "border-left: 3px solid #ccc; padding: 10px; color: #ccc; font-family: serif; font-style: italic; font-size: 18px", "markup": "
      #value#
      " } } diff --git a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx index 5e7f289d2f87..92bf10650c1f 100644 --- a/src/Umbraco.Web.UI/config/splashes/noNodes.aspx +++ b/src/Umbraco.Web.UI/config/splashes/noNodes.aspx @@ -27,7 +27,7 @@

      Welcome to your Umbraco installation

      -

      You're seeing the wonderful page because your website doesn't contain any published content yet.

      +

      You're seeing this wonderful page because your website doesn't contain any published content yet.

      Open Umbraco @@ -44,7 +44,7 @@

      Be a part of the community

      -

      The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we�re sure that you can get your answers from the community.

      +

      The Umbraco community is the best of its kind, be sure to visit, and if you have any questions, we’re sure that you can get your answers from the community.

      our.Umbraco →
      @@ -58,4 +58,4 @@ - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 9603b6eac8eb..008e5e404c59 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -1,12 +1,12 @@  - - + + - - - + + + @@ -35,6 +35,6 @@ - + \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 40f4a79ed24a..b0452fb6310a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -30,6 +30,9 @@ Genindlæs elementer Genudgiv hele sitet Gendan + Sæt rettigheder for siden %0% + Hvor vil du flytte + hen til i træstrukturen? Rettigheder Fortryd ændringer Send til udgivelse @@ -202,12 +205,12 @@ Færdig - + Slettede %0% element Slettede %0% elementer Slettede %0% ud af %1% element Slettede %0% ud af %1% elementer - + Udgav %0% element Udgav %0% elementer Udgav %0% ud af %1% element @@ -274,17 +277,11 @@ Vælg Se cache element Opret mappe... - Relatér til original - + Inkludér undersider Link til side - - Åbner det linket dokument i et nyt vindue eller fane - Åbner det linket dokument i fuld visning af vinduet - Åbner det linket dokument i "parent frame" - + Åben linket i et nyt vindue eller fane Link til medie - Vælg medie Vælg ikon Vælg item @@ -293,19 +290,25 @@ Vælg indhold Vælg medlem Vælg medlemsgruppe - Der er ingen parametre for denne makro - Link dit - Fjern link fra dit - + Fjern link fra dit konto - Vælg editor - Rediger de forskellige sprogversioner for ordbogselementet '%0%' herunder. Du tilføjer flere sprog under 'sprog' i menuen til venstre + + Du tilføjer flere sprog under 'sprog' i menuen til venstre + ]]> + Kulturnavn + Rediger navnet på ordbogselementet. + + + Indtast dit brugernavn @@ -319,17 +322,17 @@ Indtast nøgleord (tryk på Enter efter hvert nøgleord)... - Tillad på rodniveau + Tillad på rodniveau Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under Inhold og Mediearkiv Tilladte typer - Sammensætning af dokumenttyper + Sammensætning af dokumenttyper Opret Slet fane Beskrivelse Ny fane Fane Thumbnail - Aktiver listevisning + Aktiver listevisning Viser undersider i en søgbar liste, undersider vises ikke i indholdstræet Nuværende listevisning Den aktive listevisningsdatatype @@ -364,7 +367,7 @@ Der skete en fejl på severen - Denne filttype er blevet deaktiveret af administratoren + Denne filttype er blevet deaktiveret af administratoren OBS! Selvom CodeMirror er slået til i konfigurationen, så er den deaktiveret i Internet Explorer fordi den ikke er stabil nok. Du skal udfylde både Alias & Navn på den nye egenskabstype! Der mangler læse/skrive rettigheder til bestemte filer og mapper @@ -380,7 +383,7 @@ Du kan ikke opdele en celle, som ikke allerede er delt. Fejl i XSLT kode Din XSLT er ikke opdateret, da det indeholdt en fejl - Der er et problem med den datatype, der bruges til denn egenskab. Kontroller konfigurationen og prøv igen. + Der er et problem med den datatype, der bruges til denn egenskab. Kontroller konfigurationen og prøv igen. Om @@ -466,7 +469,8 @@ Hvilken side skal vises efter at formularen er sendt Størrelse Sortér - Indsend + Indsend + Type Skriv for at søge... Op @@ -505,22 +509,22 @@ - Tilføj fane - Tilføj egenskab - Tilføj editor - Tilføj skabelon - Tilføj child node - Tilføj child + Tilføj fane + Tilføj egenskab + Tilføj editor + Tilføj skabelon + Tilføj child node + Tilføj child - Rediger datatype + Rediger datatype - Naviger sektioner + Naviger sektioner - Genveje - Vis genveje + Genveje + Vis genveje - Brug listevisning - Tillad på rodniveau + Brug listevisning + Tillad på rodniveau @@ -538,13 +542,17 @@ Kunne ikke gemme web.config filen. Du bedes venligst manuelt ændre database forbindelses strengen. Din database er blevet fundet og identificeret som Database konfiguration - + installér knappen for at installere Umbraco %0% databasen - ]]> + ]]> + installér knappen for at installere Umbraco %0% databasen]]> Næste for at fortsætte.]]> - Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

      -

      For at fortsætte bedes du venligst rette "web.config" filen (ved at bruge Visual Studio eller dit favoritprogram), scroll til bunden, tilføj forbindelsesstrengen til din database i feltet som hedder "umbracoDbDSN" og gem filen.

      Klik på Forsøg igen knappen når du er færdig.
      Mere information om at redigere web.config her.

      ]]>
      + + Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

      +

      For at fortsætte bedes du venligst rette "web.config" filen (ved at bruge Visual Studio eller dit favoritprogram), scroll til bunden, tilføj forbindelsesstrengen til din database i feltet som hedder "umbracoDbDSN" og gem filen.

      Klik på Forsøg igen knappen når du er færdig.
      Mere information om at redigere web.config her.

      ]]> +
      Kontakt venligst din ISP hvis det er nødvendigt. Hvis du installerer på en lokal maskine eller server kan du muligvis få informationerne fra din systemadministrator.]]> Tryk på Opgradér knappen for at opgradere din database til Umbraco %0%

      Bare rolig - intet indhold vil blive slettet og alt vil stadig fungere bagefter!

      ]]>
      Tryk på Næste for at fortsætte.]]> @@ -590,8 +598,10 @@ Yderligere hjælpe og informationer Få hjælp fra vores prisvindende fællesskab, gennemse dokumentationen eller se nogle gratis videoer om hvordan du opsætter et simpelt site, hvordan du bruger pakker og en 'quick guide' til Umbraco terminologier]]> Umbraco %0% er installeret og klar til brug /web.config filen og opdatére 'AppSetting' feltet UmbracoConfigurationStatus i bunden til '%0%'.]]> - komme igang med det samme ved at klikke på "Start Umbraco" knappen nedenfor.
      Hvis du er ny med Umbraco, kan du finde masser af ressourcer på vores 'getting started' sider. -]]>
      + + komme igang med det samme ved at klikke på "Start Umbraco" knappen nedenfor.
      Hvis du er ny med Umbraco, kan du finde masser af ressourcer på vores 'getting started' sider. +]]> +
      Start UmbracoFor at administrere dit website skal du blot åbne Umbraco administrationen og begynde at tilføje indhold, opdatere skabelonerne og stylesheets'ene eller tilføje ny funktionalitet.]]> Forbindelse til databasen fejlede. Umbraco Version 3 @@ -666,12 +676,15 @@ Gå til http://%4%/#/content/content/edit/%5% for at redigere. Ha' en dejlig dag! Mange hilsner fra Umbraco robotten - ]]> - Hej %0%

      + ]]> +
      + + Hej %0%

      Dette er en automatisk mail for at informere dig om at opgaven '%1%' er blevet udførtpå siden '%2%' af brugeren '%3%'

      Opdateringssammendrag:

      %6%

      Hav en fortsat god dag!

      De bedste hilsner fra umbraco robotten

      ]]>
      +      RET       

      Opdateringssammendrag:

      %6%

      Hav en fortsat god dag!

      De bedste hilsner fra umbraco robotten

      ]]> + [%0%] Notificering om %1% udført på %2% Notificeringer @@ -692,8 +705,10 @@ Mange hilsner fra Umbraco robotten Pakken blev fjernet Pakken er på succefuld vis blevet fjernet Afinstallér pakke - -Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> + + +Bemærk: at dokumenter og medier som afhænger af denne pakke vil muligvis holde op med at virke, så vær forsigtig. Hvis i tvivl, kontakt personen som har udviklet pakken.]]> + Download opdatering fra opbevaringsbasen Opdatér pakke Opdateringsinstrukser @@ -726,6 +741,19 @@ Mange hilsner fra Umbraco robotten Hvis du blot ønsker at opsætte simpel beskyttelse ved hjælp af et enkelt login og kodeord + Udgivelsen kunne ikke udgives da publiceringsdato er sat + + + + + + + Udgivelsen fejlede fordi en overordnet side ikke er publiceret + %0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen Medtag ikke-udgivede undersider Publicerer - vent venligst... @@ -799,58 +827,58 @@ Mange hilsner fra Umbraco robotten - Kompositioner - Du har ikke tilføjet nogle faner - Tilføj ny fane - Tilføj endnu en fane - Nedarvet fra - Tilføj property - Påkrævet label + Kompositioner + Du har ikke tilføjet nogle faner + Tilføj ny fane + Tilføj endnu en fane + Nedarvet fra + Tilføj property + Påkrævet label - Aktiver listevisning - Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet + Aktiver listevisning + Konfigurer indholdet til at blive vist i en sorterbar og søgbar liste, dens børn vil ikke blive vist i træet - Tilladte skabeloner - Vælg hvilke skabeloner der er tilladt at bruge på dette indhold + Tilladte skabeloner + Vælg hvilke skabeloner der er tilladt at bruge på dette indhold - Tillad på rodniveau - Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv - Ja – indhold af denne type er tilladt i roden + Tillad på rodniveau + Kun dokumenttyper med denne indstilling aktiveret oprettes i rodniveau under inhold og mediearkiv + Ja – indhold af denne type er tilladt i roden - Tilladte typer - Tillad at oprette indhold af en specifik type under denne + Tilladte typer + Tillad at oprette indhold af en specifik type under denne - Vælg child node + Vælg child node - Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. - Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition - Der er ingen indholdstyper tilgængelige at bruge som komposition + Nedarv faner og egenskaber fra en anden dokumenttype. Nye faner vil blive tilføjet den nuværende dokumenttype eller sammenflettet hvis fanenavnene er ens. + Indholdstypen bliver brugt i en komposition og kan derfor ikke blive anvendt som komposition + Der er ingen indholdstyper tilgængelige at bruge som komposition - Tilgængelige editors - Genbrug - Editor indstillinger + Tilgængelige editors + Genbrug + Editor indstillinger - Konfiguration + Konfiguration - Ja, slet + Ja, slet - blev flyttet til - Vælg hvor - skal flyttes til + blev flyttet til + Vælg hvor + skal flyttes til - Alle dokumenttyper - Alle dokumenter - Alle medier + Alle dokumenttyper + Alle dokumenter + Alle medier - som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. - som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. - som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne dokumenttype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medietype vil blive slettet permanent. Bekræft at du også vil slette dem. + som benytter denne medlemstype vil blive slettet permanent. Bekræft at du også vil slette dem. - og alle dokumenter, som benytter denne type - og alle medier, som benytter denne type - og alle medlemmer, som benytter denne type + og alle dokumenter, som benytter denne type + og alle medier, som benytter denne type + og alle medlemmer, som benytter denne type - der bruger denne editor vil blive opdateret med de nye indstillinger + der bruger denne editor vil blive opdateret med de nye indstillinger @@ -914,8 +942,6 @@ Mange hilsner fra Umbraco robotten Annulleret Handlingen blev annulleret af et 3. part tilføjelsesprogram - Udgivelsen blev standset af et 3. parts modul - Udgivelsen kunne ikke udgives da publiceringsdato er sat Property type eksisterer allerede Egenskabstype oprettet DataType: %1%]]> @@ -929,7 +955,6 @@ Mange hilsner fra Umbraco robotten Stylesheet gemt uden fejl Datatype gemt Ordbogsnøgle gemt - Udgivelsen fejlede fordi en overordnet side ikke er publiceret Indhold publiceret og nu synligt for besøgende Indhold gemt @@ -1131,11 +1156,28 @@ Mange hilsner fra Umbraco robotten Session udløber - Validation - Valider som email - Valider som tal - Valider som Url - ...eller indtast din egen validering - Feltet er påkrævet + Validation + Valider som email + Valider som tal + Valider som Url + ...eller indtast din egen validering + Feltet er påkrævet + + + Slå URL tracker fra + Slå URL tracker til + Original URL + Viderestillet til + Der er ikke lavet nogen viderestillinger + Når en udgivet side bliver omdøbt eller flyttet, vil en viderestilling automatisk blive lavet til den nye side. + Fjern + Er du sikker på at du vil fjerne viderestillingen fra '%0%' til '%1%'? + Viderestillings URL fjernet. + Fejl under fjernelse af viderestillings URL. + Er du sikker på at du vil slå URL trackeren fra? + URL tracker er nu slået fra. + Der opstod en fejl under forsøget på at slå URL trackeren fra, der findes mere information i logfilen. + URL tracker er nu slået fra. + Der opstod en fejl under forsøget på at slå URL trackeren til, der findes mere information i logfilen. diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 838bbbe8d698..f7ac08d83710 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -28,6 +28,9 @@ Reload Republish entire site Restore + Set permissions for the page %0% + Choose where to move + to in the tree structure below Permissions Rollback Send To Publish @@ -144,12 +147,12 @@ Role Member Type No date chosen - Page Title + Link title Properties This document is published but is not visible because the parent '%0%' is unpublished - Oops: this document is published but is not in the cache (internal error - see log) - Oops: could not get the url (internal error - see log) - Oops: this document is published but its url would collide with content %0% + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% Publish Publication Status Publish at @@ -286,18 +289,12 @@ Pick item View Cache Item Create folder... - Relate to original + Include descendants The friendliest community - Link to page - Opens the linked document in a new window or tab - Opens the linked document in the full body of the window - Opens the linked document in the parent frame - Link to media - Select media Select icon Select item @@ -307,19 +304,14 @@ Select member Select member group No icons were found - There are no parameters for this macro - External login providers Exception Details Stacktrace Inner Exception - Link your Un-Link your - account - Select editor @@ -327,6 +319,12 @@ Edit the different language versions for the dictionary item '%0%' below
      You can add additional languages under the 'languages' in the menu on the left ]]> Culture Name + Edit the key of the dictionary item. + + + Enter your username @@ -819,7 +817,7 @@ To manage your website, simply open the Umbraco back office and start adding con Installing... Restarting, please wait... All done, your browser will now refresh, please wait... - + Please click finish to complete installation and reload page. Paste with full formatting (Not recommended) @@ -1335,7 +1333,7 @@ To manage your website, simply open the Umbraco back office and start adding con Custom errors successfully set to '%0%'. MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely when there's any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. MacroErrors are now set to '%0%'. + Zatwierdź Typ Szukaj W górę @@ -356,8 +356,8 @@ Możesz dodać dodatkowe języki w menu "Języki" po lewej stronie.]]> Witaj... Szerokość Tak - Reorder - I am done reordering + Zmień kolejność + Kolejność została zmieniona Kolor tła @@ -786,7 +786,7 @@ Miłego dnia!]]> Administrator Pole kategorii - TRANSLATE ME: 'Change Your Password' + Zmień hasło! TRANSLATE ME: 'You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button' Zawartość Opis @@ -800,10 +800,10 @@ Miłego dnia!]]> Sekcje Wyłącz dostęp do Umbraco Hasło - TRANSLATE ME: 'Your password has been changed!' - TRANSLATE ME: 'Please confirm the new password' - TRANSLATE ME: 'Enter your new password' - TRANSLATE ME: 'Your new password cannot be blank!' + Twoje hasło zostało zmienione! + Proszę potwierdź nowe hasło! + Wprowadź nowe hasło + Nowe hasło nie może byc puste! TRANSLATE ME: 'There was a difference between the new password and the confirmed password. Please try again!' TRANSLATE ME: 'The confirmed password doesn't match the new password!' Zastąp prawa dostępu dla węzłów potomnych diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index ff3c94b74da4..255bf62d5d9f 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -340,18 +340,11 @@ Выберите элемент Просмотр элемента кэша Создать папку... - Связать с оригиналом Самое дружелюбное сообщество - Ссылка на страницу - Открывает документ по ссылке в новом окне или вкладке браузера - Открывает документ по ссылке в полноэкранном режиме - Открывает документ по ссылке в родительском фрейме - Ссылка на медиа-файл - Выбрать медиа Выбрать значок Выбрать элемент @@ -360,19 +353,14 @@ Выбрать содержимое Выбрать участника Выбрать группу участников - Это макрос без параметров - Провайдеры аутентификации Подробное сообщение об ошибке Трассировка стека Внутренняя ошибка - Связать Разорвать связь - учетную запись - Выбрать редактор @@ -380,6 +368,12 @@ Ниже Вы можете указать различные переводы данной статьи словаря '%0%'
      Добавить другие языки можно, воспользовавшись пунктом 'Языки' в меню слева ]]> Название языка (культуры) + Редактировать элемент (ключ) словаря + + + Допустим как корневой @@ -499,6 +493,7 @@ Установить Неверно Выравнивание + Название Язык Макет Загрузка @@ -654,6 +649,7 @@ Медиа - всего в XML: %0%, всего: %1%Б с ошибками: %2% Содержимое - всего в XML: %0%, всего опубликовано: %1%, с ошибками: %2% + Сертификат Вашего веб-сайта отмечен как проверенный. Ошибка проверки сертификата: '%0%' Ошибка проверки адреса URL %0% - '%1%' Сейчас Вы %0% просматриваете сайт, используя протокол HTTPS. @@ -1065,6 +1061,23 @@ Чтобы опубликовать ранее неопубликованные документы среди дочерних, отметьте опцию Включая неопубликованные дочерние документы. ]]> + + Остановить отслеживание URL + Запустить отслеживание URL + Первоначальный URL + Перенаправлен в + На данный момент нет ни одного перенаправления + Если опубликованный документ переименовывается или меняет свое расположение в дереве, а следовательно, меняется адрес (URL), автоматически создается перенаправление на новое местоположение этого документа. + Удалить + Вы уверены, что хотите удалить перенаправление с '%0%' на '%1%'? + Перенаправление удалено. + Ошибка удаления перенаправления. + Вы уверены, что хотите остановить отслеживание URL? + Отслеживание URL в настоящий момент остановлено. + Ошибка остановки отслеживания URL, более подробные сведения находятся в системном журнале. + Отслеживание URL в настоящий момент запущено. + Ошибка запуска отслеживания URL, более подробные сведения находятся в системном журнале. + Заголовок Укажите заголовок diff --git a/src/Umbraco.Web.UI/umbraco/create/User.ascx b/src/Umbraco.Web.UI/umbraco/create/User.ascx index deb139c55797..3a0081177632 100644 --- a/src/Umbraco.Web.UI/umbraco/create/User.ascx +++ b/src/Umbraco.Web.UI/umbraco/create/User.ascx @@ -22,7 +22,7 @@ ControlToValidate="Email" ValidateEmptyText="false" OnServerValidate="EmailExistsCheck">
      diff --git a/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx b/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx index 683299e5be23..77deab2709cb 100644 --- a/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx +++ b/src/Umbraco.Web.UI/umbraco/dialogs/ChangeDocType.aspx @@ -108,16 +108,14 @@
- -
-

+

+ \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js index 46b7a15e4899..4c9017e159c8 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js +++ b/src/Umbraco.Web.UI/umbraco_client/Application/UmbracoApplicationActions.js @@ -281,7 +281,7 @@ Umbraco.Application.Actions = function() { actionRePublish: function() { /// - UmbClientMgr.openModalWindow('dialogs/republish.aspx?rnd=' + this._utils.generateRandom(), 'Republishing entire site', true, 450, 210); + UmbClientMgr.openModalWindow('dialogs/republish.aspx?rnd=' + this._utils.generateRandom(), uiKeys['actions_republish'], true, 450, 210); }, actionAssignDomain: function() { diff --git a/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js b/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js index 8c3d97c52b37..d15c1736320f 100644 --- a/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js +++ b/src/Umbraco.Web.UI/umbraco_client/FolderBrowser/Js/folderbrowser.js @@ -389,12 +389,12 @@ Umbraco.Sys.registerNamespace("Umbraco.Controls"); processData: false, success: function (data, textStatus) { if (textStatus == "error") { - alert("Oops. Could not update sort order"); + alert("Could not update sort order"); self._getChildNodes(); } }, error: function(data) { - alert("Oops. Could not update sort order. Err: " + data.statusText); + alert("Could not update sort order. Err: " + data.statusText); self._getChildNodes(); } }); diff --git a/src/Umbraco.Web.UI/web.Template.config b/src/Umbraco.Web.UI/web.Template.config index ffc888962544..ec7948a09a98 100644 --- a/src/Umbraco.Web.UI/web.Template.config +++ b/src/Umbraco.Web.UI/web.Template.config @@ -381,6 +381,11 @@ + + + + + diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 730efd380b53..23edb2dde21d 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -60,7 +60,7 @@ private void UmbracoModule_RouteAttempt(object sender, RoutableAttemptEventArgs } } - private void UmbracoModule_EndRequest(object sender, EventArgs e) + private void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) { // will clear the batch - will remain in HttpContext though - that's ok FlushBatch(); @@ -84,11 +84,16 @@ public void FlushBatch() var instructions = batch.SelectMany(x => x.Instructions).ToArray(); batch.Clear(); - if (instructions.Length == 0) return; - WriteInstructions(instructions); + + //Write the instructions but only create JSON blobs with a max instruction count equal to MaxProcessingInstructionCount + foreach (var instructionsBatch in instructions.InGroupsOf(Options.MaxProcessingInstructionCount)) + { + WriteInstructions(instructionsBatch); + } + } - private void WriteInstructions(RefreshInstruction[] instructions) + private void WriteInstructions(IEnumerable instructions) { var dto = new CacheInstructionDto { @@ -136,9 +141,18 @@ protected void BatchMessage( // batch if we can, else write to DB immediately if (batch == null) - WriteInstructions(instructions.ToArray()); + { + //only write the json blob with a maximum count of the MaxProcessingInstructionCount + foreach (var maxBatch in instructions.InGroupsOf(Options.MaxProcessingInstructionCount)) + { + WriteInstructions(maxBatch); + } + } else + { batch.Add(new RefreshInstructionEnvelope(servers, refresher, instructions)); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs index 63536ca9f7e4..805c2393a8dc 100644 --- a/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs +++ b/src/Umbraco.Web/BatchedWebServiceServerMessenger.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Web; using Umbraco.Core.Sync; +using Umbraco.Web.Routing; namespace Umbraco.Web { @@ -64,7 +65,7 @@ protected override ICollection GetBatch(bool ensureH return batch; } - void UmbracoModule_EndRequest(object sender, EventArgs e) + void UmbracoModule_EndRequest(object sender, UmbracoRequestEventArgs e) { FlushBatch(); } diff --git a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs index 15e01fd43053..78b211ee7cd9 100644 --- a/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs +++ b/src/Umbraco.Web/Cache/CacheRefresherEventHandler.cs @@ -11,6 +11,7 @@ using System.Linq; using umbraco.cms.businesslogic.web; using Umbraco.Core.Logging; +using Umbraco.Core.ObjectResolution; using Umbraco.Core.Publishing; using Content = Umbraco.Core.Models.Content; using ApplicationTree = Umbraco.Core.Models.ApplicationTree; @@ -21,6 +22,7 @@ namespace Umbraco.Web.Cache /// /// Class which listens to events on business level objects in order to invalidate the cache amongst servers when data changes /// + [Weight(int.MinValue)] public class CacheRefresherEventHandler : ApplicationEventHandler { protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplication, ApplicationContext applicationContext) @@ -104,14 +106,14 @@ protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplica //Bind to media events - MediaService.Saved += MediaServiceSaved; + MediaService.Saved += MediaServiceSaved; MediaService.Deleted += MediaServiceDeleted; MediaService.Moved += MediaServiceMoved; MediaService.Trashed += MediaServiceTrashed; MediaService.EmptiedRecycleBin += MediaServiceEmptiedRecycleBin; //Bind to content events - this is for unpublished content syncing across servers (primarily for examine) - + ContentService.Saved += ContentServiceSaved; ContentService.Deleted += ContentServiceDeleted; ContentService.Copied += ContentServiceCopied; @@ -125,9 +127,12 @@ protected override void ApplicationStarted(UmbracoApplicationBase umbracoApplica //public access events PublicAccessService.Saved += PublicAccessService_Saved; - PublicAccessService.Deleted += PublicAccessService_Deleted; ; + PublicAccessService.Deleted += PublicAccessService_Deleted; + + RelationService.SavedRelationType += RelationType_Saved; + RelationService.DeletedRelationType += RelationType_Deleted; } - + #region Publishing void PublishingStrategy_UnPublished(IPublishingStrategy sender, PublishEventArgs e) @@ -228,7 +233,7 @@ static void ContentServiceEmptiedRecycleBin(IContentService sender, RecycleBinEv DistributedCache.Instance.RemoveUnpublishedCachePermanently(e.Ids.ToArray()); } } - + /// /// Handles cache refreshing for when content is trashed /// @@ -250,7 +255,7 @@ static void ContentServiceTrashed(IContentService sender, MoveEventArgs /// /// - /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the + /// When an entity is copied new permissions may be assigned to it based on it's parent, if that is the /// case then we need to clear all user permissions cache. /// static void ContentServiceCopied(IContentService sender, CopyEventArgs e) @@ -282,10 +287,10 @@ static void ContentServiceDeleted(IContentService sender, DeleteEventArgs /// /// - /// When an entity is saved we need to notify other servers about the change in order for the Examine indexes to + /// When an entity is saved we need to notify other servers about the change in order for the Examine indexes to /// stay up-to-date for unpublished content. - /// - /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the + /// + /// When an entity is created new permissions may be assigned to it based on it's parent, if that is the /// case then we need to clear all user permissions cache. /// static void ContentServiceSaved(IContentService sender, SaveEventArgs e) @@ -300,8 +305,8 @@ static void ContentServiceSaved(IContentService sender, SaveEventArgs var permissionsChanged = ((Content)x).WasPropertyDirty("PermissionsChanged"); if (permissionsChanged) { - clearUserPermissions = true; - } + clearUserPermissions = true; + } } }); @@ -334,7 +339,7 @@ static void ApplicationTreeUpdated(ApplicationTree sender, EventArgs e) static void ApplicationTreeDeleted(ApplicationTree sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationTreeCache(); - } + } #endregion #region Application event handlers @@ -346,7 +351,7 @@ static void ApplicationNew(Section sender, EventArgs e) static void ApplicationDeleted(Section sender, EventArgs e) { DistributedCache.Instance.RefreshAllApplicationCache(); - } + } #endregion #region UserType event handlers @@ -359,9 +364,9 @@ static void UserServiceSavedUserType(IUserService sender, SaveEventArgs DistributedCache.Instance.RefreshUserTypeCache(x.Id)); } - + #endregion - + #region Dictionary event handlers static void LocalizationServiceSavedDictionaryItem(ILocalizationService sender, SaveEventArgs e) @@ -387,11 +392,11 @@ static void DataTypeServiceDeleted(IDataTypeService sender, DeleteEventArgs DistributedCache.Instance.RemoveDataTypeCache(x)); } - + #endregion #region Stylesheet and stylesheet property event handlers - + static void FileServiceDeletedStylesheet(IFileService sender, DeleteEventArgs e) { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveStylesheetCache(x)); @@ -438,7 +443,7 @@ static void LocalizationServiceSavedLanguage(ILocalizationService sender, SaveEv { e.SavedEntities.ForEach(x => DistributedCache.Instance.RefreshLanguageCache(x)); } - + #endregion #region Content/media/member Type event handlers @@ -502,9 +507,9 @@ static void MemberTypeServiceSaved(IMemberTypeService sender, SaveEventArgs DistributedCache.Instance.RefreshMemberTypeCache(x)); } - + #endregion - + #region User/permissions event handlers static void CacheRefresherEventHandler_AssignedPermissions(PermissionRepository sender, SaveEventArgs e) @@ -537,7 +542,7 @@ static void UserServiceDeletedUser(IUserService sender, DeleteEventArgs e { e.DeletedEntities.ForEach(x => DistributedCache.Instance.RemoveUserCache(x.Id)); } - + private static void InvalidateCacheForPermissionsChange(UserPermission sender) { if (sender.User != null) @@ -577,7 +582,7 @@ static void FileServiceSavedTemplate(IFileService sender, SaveEventArgs DistributedCache.Instance.RefreshTemplateCache(x.Id)); } - + #endregion #region Macro event handlers @@ -597,7 +602,7 @@ void MacroServiceSaved(IMacroService sender, SaveEventArgs e) DistributedCache.Instance.RefreshMacroCache(entity); } } - + #endregion #region Media event handlers @@ -628,14 +633,14 @@ static void MediaServiceDeleted(IMediaService sender, DeleteEventArgs e) static void MediaServiceSaved(IMediaService sender, SaveEventArgs e) { DistributedCache.Instance.RefreshMediaCache(e.SavedEntities.ToArray()); - } + } #endregion #region Member event handlers static void MemberServiceDeleted(IMemberService sender, DeleteEventArgs e) { - DistributedCache.Instance.RemoveMemberCache(e.DeletedEntities.ToArray()); + DistributedCache.Instance.RemoveMemberCache(e.DeletedEntities.ToArray()); } static void MemberServiceSaved(IMemberService sender, SaveEventArgs e) @@ -661,7 +666,25 @@ static void MemberGroupService_Saved(IMemberGroupService sender, SaveEventArgs args) + { + var dc = DistributedCache.Instance; + foreach (var e in args.SavedEntities) + dc.RefreshRelationTypeCache(e.Id); + } + + private static void RelationType_Deleted(IRelationService sender, DeleteEventArgs args) + { + var dc = DistributedCache.Instance; + foreach (var e in args.DeletedEntities) + dc.RemoveRelationTypeCache(e.Id); + } + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs index 44a6efe9ffef..246571d4796a 100644 --- a/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/ContentTypeCacheRefresher.cs @@ -32,7 +32,7 @@ public sealed class ContentTypeCacheRefresher : JsonCacheRefresherBase /// /// - private static JsonPayload[] DeserializeFromJsonPayload(string json) + internal static JsonPayload[] DeserializeFromJsonPayload(string json) { var serializer = new JavaScriptSerializer(); var jsonObject = serializer.Deserialize(json); @@ -45,30 +45,28 @@ private static JsonPayload[] DeserializeFromJsonPayload(string json) /// /// if the item was deleted /// - private static JsonPayload FromContentType(IContentTypeBase contentType, bool isDeleted = false) + internal static JsonPayload FromContentType(IContentTypeBase contentType, bool isDeleted = false) { var payload = new JsonPayload - { - Alias = contentType.Alias, - Id = contentType.Id, - PropertyTypeIds = contentType.PropertyTypes.Select(x => x.Id).ToArray(), - //either IContentType or IMediaType or IMemberType - Type = (contentType is IContentType) - ? typeof(IContentType).Name - : (contentType is IMediaType) + { + Alias = contentType.Alias, + Id = contentType.Id, + PropertyTypeIds = contentType.PropertyTypes.Select(x => x.Id).ToArray(), + //either IContentType or IMediaType or IMemberType + Type = (contentType is IContentType) + ? typeof(IContentType).Name + : (contentType is IMediaType) ? typeof(IMediaType).Name : typeof(IMemberType).Name, - DescendantPayloads = contentType.Descendants().Select(x => FromContentType(x)).ToArray(), - WasDeleted = isDeleted - }; - //here we need to check if the alias of the content type changed or if one of the properties was removed. - var dirty = contentType as IRememberBeingDirty; - if (dirty != null) - { - payload.PropertyRemoved = dirty.WasPropertyDirty("HasPropertyTypeBeenRemoved"); - payload.AliasChanged = dirty.WasPropertyDirty("Alias"); - payload.IsNew = dirty.WasPropertyDirty("HasIdentity"); - } + DescendantPayloads = contentType.Descendants().Select(x => FromContentType(x)).ToArray(), + WasDeleted = isDeleted, + PropertyRemoved = contentType.WasPropertyDirty("HasPropertyTypeBeenRemoved"), + AliasChanged = contentType.WasPropertyDirty("Alias"), + PropertyTypeAliasChanged = contentType.PropertyTypes.Any(x => x.WasPropertyDirty("Alias")), + IsNew = contentType.WasPropertyDirty("HasIdentity") + }; + + return payload; } @@ -90,7 +88,7 @@ internal static string SerializeToJsonPayload(bool isDeleted, params IContentTyp #region Sub classes - private class JsonPayload + internal class JsonPayload { public JsonPayload() { @@ -103,6 +101,7 @@ public JsonPayload() public string Type { get; set; } public bool AliasChanged { get; set; } public bool PropertyRemoved { get; set; } + public bool PropertyTypeAliasChanged { get; set; } public JsonPayload[] DescendantPayloads { get; set; } public bool WasDeleted { get; set; } public bool IsNew { get; set; } @@ -190,21 +189,21 @@ private void ClearContentTypeCache(JsonPayload[] payloads) ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); - payloads.ForEach(payload => + foreach (var payload in payloads) + { + //clear the cache for each item + ClearContentTypeCache(payload); + + //we only need to do this for IContentType NOT for IMediaType, we don't want to refresh the whole cache. + //if the item was deleted or the alias changed or property removed then we need to refresh the content. + //and, don't refresh the cache if it is new. + if (payload.Type == typeof(IContentType).Name + && payload.IsNew == false + && (payload.WasDeleted || payload.AliasChanged || payload.PropertyRemoved || payload.PropertyTypeAliasChanged)) { - //clear the cache for each item - ClearContentTypeCache(payload); - - //we only need to do this for IContentType NOT for IMediaType, we don't want to refresh the whole cache. - //if the item was deleted or the alias changed or property removed then we need to refresh the content. - //and, don't refresh the cache if it is new. - if (payload.Type == typeof(IContentType).Name - && !payload.IsNew - && (payload.WasDeleted || payload.AliasChanged || payload.PropertyRemoved)) - { - needsContentRefresh = true; - } - }); + needsContentRefresh = true; + } + } //need to refresh the xml content cache if required if (needsContentRefresh) @@ -237,7 +236,7 @@ private void ClearContentTypeCache(JsonPayload[] payloads) //cache if only a media type has changed. //we don't want to update the routes cache if all of the content types here are new. if (payloads.Any(x => x.Type == typeof(IContentType).Name) - && !payloads.All(x => x.IsNew)) //if they are all new then don't proceed + && payloads.All(x => x.IsNew) == false) //if they are all new then don't proceed { // SD: we need to clear the routes cache here! // diff --git a/src/Umbraco.Web/Cache/DistributedCache.cs b/src/Umbraco.Web/Cache/DistributedCache.cs index 6848ce249619..01eaf4cdd301 100644 --- a/src/Umbraco.Web/Cache/DistributedCache.cs +++ b/src/Umbraco.Web/Cache/DistributedCache.cs @@ -38,6 +38,7 @@ public sealed class DistributedCache public const string ContentTypeCacheRefresherId = "6902E22C-9C10-483C-91F3-66B7CAE9E2F5"; public const string LanguageCacheRefresherId = "3E0F95D8-0BE5-44B8-8394-2B8750B62654"; public const string DomainCacheRefresherId = "11290A79-4B57-4C99-AD72-7748A3CF38AF"; + public const string RelationTypeCacheRefresherId = "D8375ABA-4FB3-4F86-B505-92FBA1B6F7C9"; [Obsolete("This is no longer used and will be removed in future versions")] [EditorBrowsable(EditorBrowsableState.Never)] @@ -67,6 +68,7 @@ public sealed class DistributedCache public static readonly Guid DataTypeCacheRefresherGuid = new Guid(DataTypeCacheRefresherId); public static readonly Guid DictionaryCacheRefresherGuid = new Guid(DictionaryCacheRefresherId); public static readonly Guid PublicAccessCacheRefresherGuid = new Guid(PublicAccessCacheRefresherId); + public static readonly Guid RelationTypeCacheRefresherGuid = new Guid(RelationTypeCacheRefresherId); #endregion diff --git a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs index 750872d8af2f..50fd53ce09ed 100644 --- a/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Web/Cache/DistributedCacheExtensions.cs @@ -446,5 +446,19 @@ public static void ClearXsltCacheOnCurrentServer(this DistributedCache dc) } #endregion + + #region Relation type cache + + public static void RefreshRelationTypeCache(this DistributedCache dc, int id) + { + dc.Refresh(DistributedCache.RelationTypeCacheRefresherGuid, id); + } + + public static void RemoveRelationTypeCache(this DistributedCache dc, int id) + { + dc.Remove(DistributedCache.RelationTypeCacheRefresherGuid, id); + } + + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs b/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs new file mode 100644 index 000000000000..cef308c52b21 --- /dev/null +++ b/src/Umbraco.Web/Cache/RelationTypeCacheRefresher.cs @@ -0,0 +1,52 @@ +using System; +using Umbraco.Core; +using Umbraco.Core.Cache; +using Umbraco.Core.Models; +using Umbraco.Core.Persistence.Repositories; + +namespace Umbraco.Web.Cache +{ + public sealed class RelationTypeCacheRefresher : CacheRefresherBase + { + protected override RelationTypeCacheRefresher Instance + { + get { return this; } + } + + public override Guid UniqueIdentifier + { + get { return DistributedCache.RelationTypeCacheRefresherGuid; } + } + + public override string Name + { + get { return "Relation Type Cache Refresher"; } + } + + public override void RefreshAll() + { + ClearAllIsolatedCacheByEntityType(); + base.RefreshAll(); + } + + public override void Refresh(int id) + { + var cache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (cache) cache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + base.Refresh(id); + } + + public override void Refresh(Guid id) + { + throw new NotSupportedException(); + //base.Refresh(id); + } + + public override void Remove(int id) + { + var cache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache(); + if (cache) cache.Result.ClearCacheItem(RepositoryBase.GetCacheIdKey(id)); + base.Remove(id); + } + } +} diff --git a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs index beb5b8db7d5f..a36ac657a711 100644 --- a/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs +++ b/src/Umbraco.Web/Cache/UnpublishedPageCacheRefresher.cs @@ -77,6 +77,8 @@ internal class JsonPayload public override void RefreshAll() { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ClearAllIsolatedCacheByEntityType(); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); @@ -85,6 +87,8 @@ public override void RefreshAll() public override void Refresh(int id) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(id); @@ -94,6 +98,8 @@ public override void Refresh(int id) public override void Remove(int id) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ClearRepositoryCacheItemById(id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); @@ -103,6 +109,8 @@ public override void Remove(int id) public override void Refresh(IContent instance) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); content.Instance.UpdateSortOrder(instance); @@ -112,6 +120,8 @@ public override void Refresh(IContent instance) public override void Remove(IContent instance) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ClearRepositoryCacheItemById(instance.Id); ClearAllIsolatedCacheByEntityType(); DistributedCache.Instance.ClearDomainCacheOnCurrentServer(); @@ -124,6 +134,8 @@ public override void Remove(IContent instance) /// public void Refresh(string jsonPayload) { + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.IdToKeyCacheKey); + ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey); ClearAllIsolatedCacheByEntityType(); foreach (var payload in DeserializeFromJsonPayload(jsonPayload)) diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index 426adcf02f3f..14dbdac1d402 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -215,7 +215,7 @@ public async Task PostRequestPasswordReset(RequestPasswordR if (user != null && user.IsLockedOut == false) { var code = await UserManager.GeneratePasswordResetTokenAsync(identityUser.Id); - var callbackUrl = ConstuctCallbackUrl(identityUser.Id, code); + var callbackUrl = ConstructCallbackUrl(identityUser.Id, code); var message = Services.TextService.Localize("resetPasswordEmailCopyFormat", //Ensure the culture of the found user is used for the email! @@ -233,12 +233,11 @@ await UserManager.SendEmailAsync(identityUser.Id, return Request.CreateResponse(HttpStatusCode.OK); } - private string ConstuctCallbackUrl(int userId, string code) + private string ConstructCallbackUrl(int userId, string code) { - //get an mvc helper to get the url + // Get an mvc helper to get the url var http = EnsureHttpContext(); var urlHelper = new UrlHelper(http.Request.RequestContext); - var action = urlHelper.Action("ValidatePasswordResetCode", "BackOffice", new { @@ -247,12 +246,10 @@ private string ConstuctCallbackUrl(int userId, string code) r = code }); - //TODO: Virtual path? - - return string.Format("{0}://{1}{2}", - http.Request.Url.Scheme, - http.Request.Url.Host + (http.Request.Url.Port == 80 ? string.Empty : ":" + http.Request.Url.Port), - action); + // Construct full URL using configured application URL (which will fall back to request) + var applicationUri = new Uri(ApplicationContext.UmbracoApplicationUrl); + var callbackUri = new Uri(applicationUri, action); + return callbackUri.ToString(); } /// diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index d003636aacd3..58494369f5c5 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -467,7 +467,7 @@ public HttpResponseMessage EmptyRecycleBin() } /// - /// Change the sort order for media + /// Change the sort order for content /// /// /// @@ -485,23 +485,24 @@ public HttpResponseMessage PostSort(ContentSortOrder sorted) return Request.CreateResponse(HttpStatusCode.OK); } - var contentService = Services.ContentService; - var sortedContent = new List(); try { - sortedContent.AddRange(Services.ContentService.GetByIds(sorted.IdSortOrder)); + var contentService = Services.ContentService; + + // content service GetByIds does order the content items based on the order of Ids passed in + var content = contentService.GetByIds(sorted.IdSortOrder); // Save content with new sort order and update content xml in db accordingly - if (contentService.Sort(sortedContent) == false) + if (contentService.Sort(content) == false) { - LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); + LogHelper.Warn("Content sorting failed, this was probably caused by an event being cancelled"); return Request.CreateValidationErrorResponse("Content sorting failed, this was probably caused by an event being cancelled"); } return Request.CreateResponse(HttpStatusCode.OK); } catch (Exception ex) { - LogHelper.Error("Could not update content sort order", ex); + LogHelper.Error("Could not update content sort order", ex); throw; } } diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 701cca0bb2bc..d668399e8368 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -4,16 +4,125 @@ using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using System.Linq; -using System.Xml; using Umbraco.Core.IO; +using Newtonsoft.Json.Linq; +using System.Threading.Tasks; +using System.Net.Http; +using System.Web.Http; +using System; +using System.Net; +using System.Text; +using Umbraco.Core.Cache; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Umbraco.Core.Logging; namespace Umbraco.Web.Editors { + //we need to fire up the controller like this to enable loading of remote css directly from this controller [PluginController("UmbracoApi")] - public class DashboardController : UmbracoAuthorizedJsonController + [ValidationFilter] + [AngularJsonOnlyConfiguration] + [IsBackOffice] + [WebApi.UmbracoAuthorize] + public class DashboardController : UmbracoApiController { + //we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side + [ValidateAngularAntiForgeryToken] + public async Task GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/") + { + var context = UmbracoContext.Current; + if (context == null) + throw new HttpResponseException(HttpStatusCode.InternalServerError); + + var user = Security.CurrentUser; + var userType = user.UserType.Alias; + var allowedSections = string.Join(",", user.AllowedSections); + var language = user.Language; + var version = UmbracoVersion.GetSemanticVersion().ToSemanticString(); + + var url = string.Format(baseUrl + "{0}?section={0}&type={1}&allowed={2}&lang={3}&version={4}", section, userType, allowedSections, language, version); + var key = "umbraco-dynamic-dashboard-" + userType + language + allowedSections.Replace(",", "-") + section; + + var content = ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem(key); + var result = new JObject(); + if (content != null) + { + result = content; + } + else + { + //content is null, go get it + try + { + using (var web = new HttpClient()) + { + //fetch dashboard json and parse to JObject + var json = await web.GetStringAsync(url); + content = JObject.Parse(json); + result = content; + } + + ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); + } + catch (HttpRequestException ex) + { + LogHelper.Debug(string.Format("Error getting dashboard content from '{0}': {1}\n{2}", url, ex.Message, ex.InnerException)); + + //it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings + ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); + } + } + + return result; + } + public async Task GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/") + { + var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section); + var key = "umbraco-dynamic-dashboard-css-" + section; + + var content = ApplicationContext.ApplicationCache.RuntimeCache.GetCacheItem(key); + var result = string.Empty; + + if (content != null) + { + result = content; + } + else + { + //content is null, go get it + try + { + using (var web = new HttpClient()) + { + //fetch remote css + content = await web.GetStringAsync(url); + + //can't use content directly, modified closure problem + result = content; + + //save server content for 30 mins + ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 30, 0)); + } + } + catch (HttpRequestException ex) + { + LogHelper.Debug(string.Format("Error getting dashboard CSS from '{0}': {1}\n{2}", url, ex.Message, ex.InnerException)); + + //it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings + ApplicationContext.ApplicationCache.RuntimeCache.InsertCacheItem(key, () => result, new TimeSpan(0, 5, 0)); + } + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(result, Encoding.UTF8, "text/css") + }; + } + + [ValidateAngularAntiForgeryToken] public IEnumerable> GetDashboard(string section) { var tabs = new List>(); @@ -23,50 +132,47 @@ public IEnumerable> GetDashboard(string section) foreach( var dashboardSection in UmbracoConfig.For.DashboardSettings().Sections.Where(x => x.Areas.Contains(section))) { //we need to validate access to this section - if (DashboardSecurity.AuthorizeAccess(dashboardSection, Security.CurrentUser, Services.SectionService)) + if (DashboardSecurity.AuthorizeAccess(dashboardSection, Security.CurrentUser, Services.SectionService) == false) + continue; + + //User is authorized + foreach (var tab in dashboardSection.Tabs) { - //User is authorized - foreach (var dashTab in dashboardSection.Tabs) + //we need to validate access to this tab + if (DashboardSecurity.AuthorizeAccess(tab, Security.CurrentUser, Services.SectionService) == false) + continue; + + var dashboardControls = new List(); + + foreach (var control in tab.Controls) { - //we need to validate access to this tab - if (DashboardSecurity.AuthorizeAccess(dashTab, Security.CurrentUser, Services.SectionService)) - { - var props = new List(); - - foreach (var dashCtrl in dashTab.Controls) - { - if (DashboardSecurity.AuthorizeAccess(dashCtrl, Security.CurrentUser, - Services.SectionService)) - { - var ctrl = new DashboardControl(); - var controlPath = dashCtrl.ControlPath.Trim(' ', '\r', '\n'); - ctrl.Path = IOHelper.FindFile(controlPath); - if (controlPath.ToLower().EndsWith(".ascx")) - { - ctrl.ServerSide = true; - } - props.Add(ctrl); - } - } - - tabs.Add(new Tab - { - Id = i, - Alias = dashTab.Caption.ToSafeAlias(), - IsActive = i == 1, - Label = dashTab.Caption, - Properties = props - }); - i++; - } + if (DashboardSecurity.AuthorizeAccess(control, Security.CurrentUser, Services.SectionService) == false) + continue; + + var dashboardControl = new DashboardControl(); + var controlPath = control.ControlPath.Trim(); + dashboardControl.Path = IOHelper.FindFile(controlPath); + if (controlPath.ToLowerInvariant().EndsWith(".ascx".ToLowerInvariant())) + dashboardControl.ServerSide = true; + + dashboardControls.Add(dashboardControl); } + + tabs.Add(new Tab + { + Id = i, + Alias = tab.Caption.ToSafeAlias(), + IsActive = i == 1, + Label = tab.Caption, + Properties = dashboardControls + }); + + i++; } } //In case there are no tabs or a user doesn't have access the empty tabs list is returned return tabs; - } - } } diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 4e3bf1a0ec69..7a7e349a3c9c 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -65,7 +65,7 @@ public MediaController(UmbracoContext umbracoContext) } /// - /// Gets an empty content item for the + /// Gets an empty content item for the /// /// /// @@ -213,7 +213,7 @@ public PagedResult> GetChildren(i /// Moves an item to the recycle bin, if it is already there then it will permanently delete it /// /// - /// + /// [EnsureUserPermissionForMedia("id")] [HttpPost] public HttpResponseMessage DeleteById(int id) @@ -231,7 +231,7 @@ public HttpResponseMessage DeleteById(int id) var moveResult = Services.MediaService.WithResult().MoveToRecycleBin(foundMedia, (int)Security.CurrentUser.Id); if (moveResult == false) { - //returning an object of INotificationModel will ensure that any pending + //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); } @@ -241,7 +241,7 @@ public HttpResponseMessage DeleteById(int id) var deleteResult = Services.MediaService.WithResult().Delete(foundMedia, (int)Security.CurrentUser.Id); if (deleteResult == false) { - //returning an object of INotificationModel will ensure that any pending + //returning an object of INotificationModel will ensure that any pending // notification messages are added to the response. return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); } @@ -270,7 +270,7 @@ public HttpResponseMessage PostMove(MoveOrCopy move) /// /// Saves content /// - /// + /// [FileUploadCleanupFilter] [MediaPostValidate] public MediaItemDisplay PostSave( @@ -290,7 +290,7 @@ public MediaItemDisplay PostSave( // * We still need to save the entity even if there are validation value errors // * Depending on if the entity is new, and if there are non property validation errors (i.e. the name is null) // then we cannot continue saving, we can only display errors - // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display + // * If there are validation errors and they were attempting to publish, we can only save, NOT publish and display // a message indicating this if (ModelState.IsValid == false) { @@ -314,7 +314,7 @@ public MediaItemDisplay PostSave( //lasty, if it is not valid, add the modelstate to the outgoing object and throw a 403 HandleInvalidModelState(display); - //put the correct msgs in + //put the correct msgs in switch (contentItem.Action) { case ContentSaveAction.Save: @@ -461,7 +461,7 @@ public async Task PostAddFile() Services.MediaService, parentId) == false) { return Request.CreateResponse( - HttpStatusCode.Unauthorized, + HttpStatusCode.Forbidden, new SimpleNotificationModel(new Notification( Services.TextService.Localize("speechBubbles/operationFailedHeader"), Services.TextService.Localize("speechBubbles/invalidUserPermissionsText"), @@ -524,17 +524,26 @@ public async Task PostAddFile() //get the files foreach (var file in result.FileData) { - var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }); + var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd(); var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower(); if (UmbracoConfig.For.UmbracoSettings().Content.DisallowedUploadFiles.Contains(ext) == false) { var mediaType = Constants.Conventions.MediaTypes.File; - if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) - mediaType = Constants.Conventions.MediaTypes.Image; + if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect) + { + if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.Contains(ext)) + { + mediaType = Constants.Conventions.MediaTypes.Image; + } + } + else + { + mediaType = result.FormData["contentTypeAlias"]; + } - //TODO: make the media item name "nice" since file names could be pretty ugly, we have + //TODO: make the media item name "nice" since file names could be pretty ugly, we have // string extensions to do much of this but we'll need: // * Pascalcase the name (use string extensions) // * strip the file extension @@ -595,7 +604,7 @@ public async Task PostAddFile() return Request.CreateResponse(HttpStatusCode.OK, tempFiles); } - + /// /// Ensures the item can be moved/copied to the new location /// @@ -654,7 +663,7 @@ private IMedia ValidateMoveOrCopy(MoveOrCopy model) } /// - /// Performs a permissions check for the user to check if it has access to the node based on + /// Performs a permissions check for the user to check if it has access to the node based on /// start node and/or permissions for the node /// /// The storage to add the content item to so it can be reused @@ -668,7 +677,7 @@ internal static bool CheckPermissions(IDictionary storage, IUser if (media == null && nodeId != Constants.System.Root && nodeId != Constants.System.RecycleBinMedia) { media = mediaService.GetById(nodeId); - //put the content item into storage so it can be retreived + //put the content item into storage so it can be retreived // in the controller (saves a lookup) storage[typeof(IMedia).ToString()] = media; } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs index 654d7dd20935..2b9f1aa2a2a4 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Config/TrySkipIisCustomErrorsCheck.cs @@ -38,7 +38,8 @@ public override IEnumerable Values { get { - var recommendedValue = _serverVersion >= new Version("7.5.0") + // beware! 7.5 and 7.5.0 are not the same thing! + var recommendedValue = _serverVersion >= new Version("7.5") ? bool.TrueString.ToLower() : bool.FalseString.ToLower(); return new List { new AcceptableConfiguration { IsRecommended = true, Value = recommendedValue } }; @@ -50,7 +51,7 @@ public override string CheckSuccessMessage get { return _textService.Localize("healthcheck/trySkipIisCustomErrorsCheckSuccessMessage", - new[] { CurrentValue, Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); + new[] { Values.First(v => v.IsRecommended).Value, _serverVersion.ToString() }); } } diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs index af1b15818a31..cf279bf3f8e0 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/ExcessiveHeadersCheck.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Net; using System.Web; +using Umbraco.Core.Configuration; using Umbraco.Core.Services; namespace Umbraco.Web.HealthCheck.Checks.Security @@ -10,7 +11,7 @@ namespace Umbraco.Web.HealthCheck.Checks.Security [HealthCheck( "92ABBAA2-0586-4089-8AE2-9A843439D577", "Excessive Headers", - Description = "Checks to see if your site is revealing information in it's headers that gives away unnecessary details about the technology used to build and host it.", + Description = "Checks to see if your site is revealing information in its headers that gives away unnecessary details about the technology used to build and host it.", Group = "Security")] public class ExcessiveHeadersCheck : HealthCheck { @@ -45,10 +46,11 @@ private HealthCheckStatus CheckForHeaders() { var message = string.Empty; var success = false; - var url = HealthCheckContext.HttpContext.Request.Url; - - // Access the site home page and check for the headers - var address = string.Format("http://{0}:{1}", url.Host.ToLower(), url.Port); + var url = HealthCheckContext.HttpContext.Request.Url; + + // Access the site home page and check for the headers + var useSsl = GlobalSettings.UseSSL || HealthCheckContext.HttpContext.Request.ServerVariables["SERVER_PORT"] == "443"; + var address = string.Format("http{0}://{1}:{2}", useSsl ? "s" : "", url.Host.ToLower(), url.Port); var request = WebRequest.Create(address); request.Method = "HEAD"; try diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs index 80853c01d895..4e2dc4f8f5e4 100644 --- a/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/HttpsCheck.cs @@ -84,6 +84,9 @@ private HealthCheckStatus CheckForValidCertificate() var actions = new List(); + if (success) + message = _textService.Localize("healthcheck/httpsCheckValidCertificate"); + return new HealthCheckStatus(message) { diff --git a/src/Umbraco.Web/HtmlStringUtilities.cs b/src/Umbraco.Web/HtmlStringUtilities.cs index 5ba1d17f4e01..24a643b5b0cb 100644 --- a/src/Umbraco.Web/HtmlStringUtilities.cs +++ b/src/Umbraco.Web/HtmlStringUtilities.cs @@ -211,6 +211,13 @@ public IHtmlString Truncate(string html, int length, bool addElipsis, bool treat if (!lengthReached && currentTextLength >= length) { + // if the last character added was the first of a two character unicode pair, add the second character + if (Char.IsHighSurrogate((char)ic)) + { + var lowSurrogate = tr.Read(); + outputtw.Write((char)lowSurrogate); + } + // Reached truncate limit. if (addElipsis) { diff --git a/src/Umbraco.Web/Media/ThumbnailProviders/FileExtensionIconThumbnailProvider.cs b/src/Umbraco.Web/Media/ThumbnailProviders/FileExtensionIconThumbnailProvider.cs index 4d6489cb2cb8..869959246773 100644 --- a/src/Umbraco.Web/Media/ThumbnailProviders/FileExtensionIconThumbnailProvider.cs +++ b/src/Umbraco.Web/Media/ThumbnailProviders/FileExtensionIconThumbnailProvider.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.Media.ThumbnailProviders { - [WeightedPlugin(2000)] + [Weight(2000)] public class FileExtensionIconThumbnailProvider : AbstractThumbnailProvider { protected override IEnumerable SupportedExtensions diff --git a/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs b/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs index 6f9b26c79d46..56fe6306e39b 100644 --- a/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs +++ b/src/Umbraco.Web/Media/ThumbnailProviders/ImageThumbnailProvider.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.Media.ThumbnailProviders { - [WeightedPlugin(1000)] + [Weight(1000)] public class ImageThumbnailProvider : AbstractThumbnailProvider { protected override IEnumerable SupportedExtensions diff --git a/src/Umbraco.Web/Media/ThumbnailProviders/MediaTypeIconThumbnailProvider.cs b/src/Umbraco.Web/Media/ThumbnailProviders/MediaTypeIconThumbnailProvider.cs index be1e6b4b4279..6112d6047140 100644 --- a/src/Umbraco.Web/Media/ThumbnailProviders/MediaTypeIconThumbnailProvider.cs +++ b/src/Umbraco.Web/Media/ThumbnailProviders/MediaTypeIconThumbnailProvider.cs @@ -8,7 +8,7 @@ namespace Umbraco.Web.Media.ThumbnailProviders { - [WeightedPlugin(3000)] + [Weight(3000)] public class MediaTypeIconThumbnailProvider : AbstractThumbnailProvider { diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index e2cdd1f7e436..e179159e7c1a 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -6,7 +6,6 @@ using System.Web.Mvc; using System.Web.Routing; using AutoMapper; -using umbraco; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; @@ -29,38 +28,18 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext //FROM IContent TO ContentItemDisplay config.CreateMap() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.Updater, - expression => expression.ResolveUsing()) - .ForMember( - dto => dto.Icon, - expression => expression.MapFrom(content => content.ContentType.Icon)) - .ForMember( - dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) - .ForMember( - dto => dto.ContentTypeName, - expression => expression.MapFrom(content => content.ContentType.Name)) - .ForMember( - dto => dto.IsContainer, - expression => expression.MapFrom(content => content.ContentType.IsContainer)) - .ForMember(display => display.IsChildOfListView, expression => expression.Ignore()) - .ForMember( - dto => dto.Trashed, - expression => expression.MapFrom(content => content.Trashed)) - .ForMember( - dto => dto.PublishDate, - expression => expression.MapFrom(content => GetPublishedDate(content, applicationContext))) - .ForMember( - dto => dto.TemplateAlias, expression => expression.MapFrom(content => content.Template.Alias)) - .ForMember( - dto => dto.HasPublishedVersion, - expression => expression.MapFrom(content => content.HasPublishedVersion)) - .ForMember( - dto => dto.Urls, + .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(display => display.Updater, expression => expression.ResolveUsing(new CreatorResolver())) + .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) + .ForMember(display => display.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember(display => display.ContentTypeName, expression => expression.MapFrom(content => content.ContentType.Name)) + .ForMember(display => display.IsContainer, expression => expression.MapFrom(content => content.ContentType.IsContainer)) + .ForMember(display => display.IsChildOfListView, expression => expression.Ignore()) + .ForMember(display => display.Trashed, expression => expression.MapFrom(content => content.Trashed)) + .ForMember(display => display.PublishDate, expression => expression.MapFrom(content => GetPublishedDate(content, applicationContext))) + .ForMember(display => display.TemplateAlias, expression => expression.MapFrom(content => content.Template.Alias)) + .ForMember(display => display.HasPublishedVersion, expression => expression.MapFrom(content => content.HasPublishedVersion)) + .ForMember(display => display.Urls, expression => expression.MapFrom(content => UmbracoContext.Current == null ? new[] {"Cannot generate urls without a current Umbraco Context"} @@ -74,47 +53,28 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) .ForMember(display => display.AllowedActions, expression => expression.ResolveUsing( new ActionButtonsResolver(new Lazy(() => applicationContext.Services.UserService)))) - .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, + .AfterMap((content, display) => AfterMap(content, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, applicationContext.Services.ContentTypeService)); //FROM IContent TO ContentItemBasic config.CreateMap>() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.Updater, - expression => expression.ResolveUsing()) - .ForMember( - dto => dto.Icon, - expression => expression.MapFrom(content => content.ContentType.Icon)) - .ForMember( - dto => dto.Trashed, - expression => expression.MapFrom(content => content.Trashed)) - .ForMember( - dto => dto.HasPublishedVersion, - expression => expression.MapFrom(content => content.HasPublishedVersion)) - .ForMember( - dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) - .ForMember(display => display.Alias, expression => expression.Ignore()); + .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(dto => dto.Updater, expression => expression.ResolveUsing(new CreatorResolver())) + .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) + .ForMember(dto => dto.Trashed, expression => expression.MapFrom(content => content.Trashed)) + .ForMember(dto => dto.HasPublishedVersion, expression => expression.MapFrom(content => content.HasPublishedVersion)) + .ForMember(dto => dto.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember(dto => dto.Alias, expression => expression.Ignore()); //FROM IContent TO ContentItemDto config.CreateMap>() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.HasPublishedVersion, - expression => expression.MapFrom(content => content.HasPublishedVersion)) - .ForMember(display => display.Updater, expression => expression.Ignore()) - .ForMember(display => display.Icon, expression => expression.Ignore()) - .ForMember(display => display.Alias, expression => expression.Ignore()); - - + .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(dto => dto.HasPublishedVersion, expression => expression.MapFrom(content => content.HasPublishedVersion)) + .ForMember(dto => dto.Updater, expression => expression.Ignore()) + .ForMember(dto => dto.Icon, expression => expression.Ignore()) + .ForMember(dto => dto.Alias, expression => expression.Ignore()); } - /// /// Maps the generic tab with custom properties for content /// @@ -123,7 +83,7 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext /// /// /// - private static void AfterMap(IContent content, ContentItemDisplay display, IDataTypeService dataTypeService, + private static void AfterMap(IContent content, ContentItemDisplay display, IDataTypeService dataTypeService, ILocalizedTextService localizedText, IContentTypeService contentTypeService) { //map the IsChildOfListView (this is actually if it is a descendant of a list view!) @@ -151,7 +111,6 @@ private static void AfterMap(IContent content, ContentItemDisplay display, IData display.IsChildOfListView = ancesctorListView != null; } } - //map the tree node url if (HttpContext.Current != null) @@ -160,9 +119,9 @@ private static void AfterMap(IContent content, ContentItemDisplay display, IData var url = urlHelper.GetUmbracoApiService(controller => controller.GetTreeNode(display.Id.ToString(), null)); display.TreeNodeUrl = url; } - + //fill in the template config to be passed to the template drop down. - var templateItemConfig = new Dictionary { { "", "Choose..." } }; + var templateItemConfig = new Dictionary {{"", "Choose..."}}; foreach (var t in content.ContentType.AllowedTemplates .Where(t => t.Alias.IsNullOrWhiteSpace() == false && t.Name.IsNullOrWhiteSpace() == false)) { @@ -173,7 +132,7 @@ private static void AfterMap(IContent content, ContentItemDisplay display, IData { TabsAndPropertiesResolver.AddListView(display, "content", dataTypeService, localizedText); } - + var properties = new List { new ContentPropertyDisplay @@ -183,26 +142,26 @@ private static void AfterMap(IContent content, ContentItemDisplay display, IData Value = localizedText.UmbracoDictionaryTranslate(display.ContentTypeName), View = PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View }, - new ContentPropertyDisplay + new ContentPropertyDisplay { Alias = string.Format("{0}releasedate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = localizedText.Localize("content/releaseDate"), Value = display.ReleaseDate.HasValue ? display.ReleaseDate.Value.ToIsoString() : null, //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, Config = new Dictionary { {"offsetTime", "1"} } //TODO: Fix up hard coded datepicker - } , + }, new ContentPropertyDisplay { Alias = string.Format("{0}expiredate", Constants.PropertyEditors.InternalGenericPropertiesPrefix), Label = localizedText.Localize("content/unpublishDate"), Value = display.ExpireDate.HasValue ? display.ExpireDate.Value.ToIsoString() : null, //Not editible for people without publish permission (U4-287) - View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, + View = display.AllowedActions.Contains(ActionPublish.Instance.Letter) ? "datepicker" : PropertyEditorResolver.Current.GetByAlias(Constants.PropertyEditors.NoEditAlias).ValueEditor.View, Config = new Dictionary { {"offsetTime", "1"} @@ -246,21 +205,21 @@ private static void AfterMap(IContent content, ContentItemDisplay display, IData var docTypeLink = string.Format("#/settings/documenttypes/edit/{0}", currentDocumentTypeId); //Replace the doc type property - var docTypeProp = genericProperties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); - docTypeProp.Value = new List + var docTypeProperty = genericProperties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + docTypeProperty.Value = new List { new { linkText = currentDocumentTypeName, url = docTypeLink, - target = "_self", icon = "icon-item-arrangement" + target = "_self", + icon = "icon-item-arrangement" } }; //TODO: Hard coding this because the templatepicker doesn't necessarily need to be a resolvable (real) property editor - docTypeProp.View = "urllist"; + docTypeProperty.View = "urllist"; } }); - } /// @@ -305,13 +264,13 @@ protected override IEnumerable ResolveCore(IContent source) var svc = _userService.Value; var permissions = svc.GetPermissions( - //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is - // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null - // refrence exception :( - UmbracoContext.Current.Security.CurrentUser, - // Here we need to do a special check since this could be new content, in which case we need to get the permissions - // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. - source.HasIdentity ? source.Id : source.ParentId) + //TODO: This is certainly not ideal usage here - perhaps the best way to deal with this in the future is + // with the IUmbracoContextAccessor. In the meantime, if used outside of a web app this will throw a null + // refrence exception :( + UmbracoContext.Current.Security.CurrentUser, + // Here we need to do a special check since this could be new content, in which case we need to get the permissions + // from the parent, not the existing one otherwise permissions would be coming from the root since Id is 0. + source.HasIdentity ? source.Id : source.ParentId) .FirstOrDefault(); return permissions == null @@ -319,6 +278,5 @@ protected override IEnumerable ResolveCore(IContent source) : permissions.AssignedPermissions.Where(x => x.Length == 1).Select(x => x.ToUpperInvariant()[0]); } } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index 70ff63ca1290..ac2faa82dc49 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -61,7 +61,7 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext }); config.CreateMap() - .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing()) + .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing(new AvailablePropertyEditorsResolver())) .ForMember(display => display.PreValues, expression => expression.ResolveUsing( new PreValueDisplayResolver(lazyDataTypeService))) .ForMember(display => display.SelectedEditor, expression => expression.MapFrom( @@ -98,7 +98,7 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext .ForMember(definition => definition.Key, expression => expression.Ignore()) .ForMember(definition => definition.Path, expression => expression.Ignore()) .ForMember(definition => definition.PropertyEditorAlias, expression => expression.MapFrom(save => save.SelectedEditor)) - .ForMember(definition => definition.DatabaseType, expression => expression.ResolveUsing()) + .ForMember(definition => definition.DatabaseType, expression => expression.ResolveUsing(new DatabaseTypeResolver())) .ForMember(x => x.ControlId, expression => expression.Ignore()) .ForMember(x => x.CreatorId, expression => expression.Ignore()) .ForMember(x => x.Level, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 44df5e4d5292..c3f941240123 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using System.Web.Routing; using AutoMapper; -using umbraco; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; @@ -29,22 +25,12 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext { //FROM IMedia TO MediaItemDisplay config.CreateMap() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.Icon, - expression => expression.MapFrom(content => content.ContentType.Icon)) - .ForMember( - dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) + .ForMember(display => display.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) .ForMember(display => display.IsChildOfListView, expression => expression.Ignore()) - .ForMember( - dto => dto.Trashed, - expression => expression.MapFrom(content => content.Trashed)) - .ForMember( - dto => dto.ContentTypeName, - expression => expression.MapFrom(content => content.ContentType.Name)) + .ForMember(display => display.Trashed, expression => expression.MapFrom(content => content.Trashed)) + .ForMember(display => display.ContentTypeName, expression => expression.MapFrom(content => content.ContentType.Name)) .ForMember(display => display.Properties, expression => expression.Ignore()) .ForMember(display => display.TreeNodeUrl, expression => expression.Ignore()) .ForMember(display => display.Notifications, expression => expression.Ignore()) @@ -53,39 +39,29 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext .ForMember(display => display.Updater, expression => expression.Ignore()) .ForMember(display => display.Alias, expression => expression.Ignore()) .ForMember(display => display.IsContainer, expression => expression.Ignore()) - .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()) + .ForMember(display => display.HasPublishedVersion, expression => expression.Ignore()) .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new TabsAndPropertiesResolver(applicationContext.Services.TextService))) .AfterMap((media, display) => AfterMap(media, display, applicationContext.Services.DataTypeService, applicationContext.Services.TextService, applicationContext.ProfilingLogger.Logger)); //FROM IMedia TO ContentItemBasic config.CreateMap>() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.Icon, - expression => expression.MapFrom(content => content.ContentType.Icon)) - .ForMember( - dto => dto.Trashed, - expression => expression.MapFrom(content => content.Trashed)) - .ForMember( - dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) - .ForMember(x => x.Published, expression => expression.Ignore()) - .ForMember(x => x.Updater, expression => expression.Ignore()) - .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()); + .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) + .ForMember(dto => dto.Trashed, expression => expression.MapFrom(content => content.Trashed)) + .ForMember(dto => dto.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember(dto => dto.Published, expression => expression.Ignore()) + .ForMember(dto => dto.Updater, expression => expression.Ignore()) + .ForMember(dto => dto.Alias, expression => expression.Ignore()) + .ForMember(dto => dto.HasPublishedVersion, expression => expression.Ignore()); //FROM IMedia TO ContentItemDto config.CreateMap>() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember(x => x.Published, expression => expression.Ignore()) - .ForMember(x => x.Updater, expression => expression.Ignore()) - .ForMember(x => x.Icon, expression => expression.Ignore()) - .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()); + .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(dto => dto.Published, expression => expression.Ignore()) + .ForMember(dto => dto.Updater, expression => expression.Ignore()) + .ForMember(dto => dto.Icon, expression => expression.Ignore()) + .ForMember(dto => dto.Alias, expression => expression.Ignore()) + .ForMember(dto => dto.HasPublishedVersion, expression => expression.Ignore()); } private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeService dataTypeService, ILocalizedTextService localizedText, ILogger logger) @@ -155,8 +131,28 @@ private static void AfterMap(IMedia media, MediaItemDisplay display, IDataTypeSe genericProperties.Add(link); } - TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties); - } + TabsAndPropertiesResolver.MapGenericProperties(media, display, localizedText, genericProperties, properties => + { + if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null + && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) + { + var mediaTypeLink = string.Format("#/settings/mediatypes/edit/{0}", media.ContentTypeId); + //Replace the doctype property + var docTypeProperty = properties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + docTypeProperty.Value = new List + { + new + { + linkText = media.ContentType.Name, + url = mediaTypeLink, + target = "_self", + icon = "icon-item-arrangement" + } + }; + docTypeProperty.View = "urllist"; + } + }); + } } } diff --git a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs index 40d563b9bca0..edb44d36ce55 100644 --- a/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MemberModelMapper.cs @@ -11,7 +11,6 @@ using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; -using umbraco; using System.Linq; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Security; @@ -29,15 +28,15 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext //FROM MembershipUser TO MediaItemDisplay - used when using a non-umbraco membership provider config.CreateMap() .ConvertUsing(user => - { - var member = Mapper.Map(user); - return Mapper.Map(member); - }); + { + var member = Mapper.Map(user); + return Mapper.Map(member); + }); //FROM MembershipUser TO IMember - used when using a non-umbraco membership provider config.CreateMap() .ConstructUsing(user => MemberService.CreateGenericMembershipProviderMember(user.UserName, user.Email, user.UserName, "")) - //we're giving this entity an ID - we cannot really map it but it needs an id so the system knows it's not a new entity + //we're giving this entity an ID of 0 - we cannot really map it but it needs an id so the system knows it's not a new entity .ForMember(member => member.Id, expression => expression.MapFrom(user => int.MaxValue)) .ForMember(member => member.Comments, expression => expression.MapFrom(user => user.Comment)) .ForMember(member => member.CreateDate, expression => expression.MapFrom(user => user.CreationDate)) @@ -62,23 +61,13 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext //FROM IMember TO MediaItemDisplay config.CreateMap() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.Icon, - expression => expression.MapFrom(content => content.ContentType.Icon)) - .ForMember( - dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) - .ForMember( - dto => dto.ContentTypeName, - expression => expression.MapFrom(content => content.ContentType.Name)) + .ForMember(display => display.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(display => display.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) + .ForMember(display => display.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember(display => display.ContentTypeName, expression => expression.MapFrom(content => content.ContentType.Name)) .ForMember(display => display.Properties, expression => expression.Ignore()) - .ForMember(display => display.Tabs, - expression => expression.ResolveUsing(new MemberTabsAndPropertiesResolver(applicationContext.Services.TextService))) - .ForMember(display => display.MemberProviderFieldMapping, - expression => expression.ResolveUsing()) + .ForMember(display => display.Tabs, expression => expression.ResolveUsing(new MemberTabsAndPropertiesResolver(applicationContext.Services.TextService))) + .ForMember(display => display.MemberProviderFieldMapping, expression => expression.ResolveUsing(new MemberProviderFieldMappingResolver())) .ForMember(display => display.MembershipScenario, expression => expression.ResolveUsing(new MembershipScenarioMappingResolver(new Lazy(() => applicationContext.Services.MemberTypeService)))) .ForMember(display => display.Notifications, expression => expression.Ignore()) @@ -90,76 +79,56 @@ public override void ConfigureMappings(IConfiguration config, ApplicationContext .ForMember(display => display.Trashed, expression => expression.Ignore()) .ForMember(display => display.IsContainer, expression => expression.Ignore()) .ForMember(display => display.TreeNodeUrl, expression => expression.Ignore()) - .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()) + .ForMember(display => display.HasPublishedVersion, expression => expression.Ignore()) .AfterMap((member, display) => MapGenericCustomProperties(applicationContext.Services.MemberService, member, display, applicationContext.Services.TextService)); //FROM IMember TO MemberBasic config.CreateMap() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember( - dto => dto.Icon, - expression => expression.MapFrom(content => content.ContentType.Icon)) - .ForMember( - dto => dto.ContentTypeAlias, - expression => expression.MapFrom(content => content.ContentType.Alias)) - .ForMember( - dto => dto.Email, - expression => expression.MapFrom(content => content.Email)) - .ForMember( - dto => dto.Username, - expression => expression.MapFrom(content => content.Username)) - .ForMember(display => display.Trashed, expression => expression.Ignore()) - .ForMember(x => x.Published, expression => expression.Ignore()) - .ForMember(x => x.Updater, expression => expression.Ignore()) - .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()); + .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(dto => dto.Icon, expression => expression.MapFrom(content => content.ContentType.Icon)) + .ForMember(dto => dto.ContentTypeAlias, expression => expression.MapFrom(content => content.ContentType.Alias)) + .ForMember(dto => dto.Email, expression => expression.MapFrom(content => content.Email)) + .ForMember(dto => dto.Username, expression => expression.MapFrom(content => content.Username)) + .ForMember(dto => dto.Trashed, expression => expression.Ignore()) + .ForMember(dto => dto.Published, expression => expression.Ignore()) + .ForMember(dto => dto.Updater, expression => expression.Ignore()) + .ForMember(dto => dto.Alias, expression => expression.Ignore()) + .ForMember(dto => dto.HasPublishedVersion, expression => expression.Ignore()); //FROM MembershipUser TO MemberBasic config.CreateMap() - //we're giving this entity an ID - we cannot really map it but it needs an id so the system knows it's not a new entity + //we're giving this entity an ID of 0 - we cannot really map it but it needs an id so the system knows it's not a new entity .ForMember(member => member.Id, expression => expression.MapFrom(user => int.MaxValue)) .ForMember(member => member.CreateDate, expression => expression.MapFrom(user => user.CreationDate)) .ForMember(member => member.UpdateDate, expression => expression.MapFrom(user => user.LastActivityDate)) .ForMember(member => member.Key, expression => expression.MapFrom(user => user.ProviderUserKey.TryConvertTo().Result.ToString("N"))) - .ForMember( - dto => dto.Owner, - expression => expression.UseValue(new UserBasic {Name = "Admin", UserId = 0})) - .ForMember( - dto => dto.Icon, - expression => expression.UseValue("icon-user")) + .ForMember(member => member.Owner, expression => expression.UseValue(new UserBasic {Name = "Admin", UserId = 0})) + .ForMember(member => member.Icon, expression => expression.UseValue("icon-user")) .ForMember(member => member.Name, expression => expression.MapFrom(user => user.UserName)) - .ForMember( - dto => dto.Email, - expression => expression.MapFrom(content => content.Email)) - .ForMember( - dto => dto.Username, - expression => expression.MapFrom(content => content.UserName)) + .ForMember(member => member.Email, expression => expression.MapFrom(content => content.Email)) + .ForMember(member => member.Username, expression => expression.MapFrom(content => content.UserName)) .ForMember(member => member.Properties, expression => expression.Ignore()) .ForMember(member => member.ParentId, expression => expression.Ignore()) .ForMember(member => member.Path, expression => expression.Ignore()) .ForMember(member => member.SortOrder, expression => expression.Ignore()) .ForMember(member => member.AdditionalData, expression => expression.Ignore()) - .ForMember(x => x.Published, expression => expression.Ignore()) - .ForMember(x => x.Updater, expression => expression.Ignore()) - .ForMember(dto => dto.Trashed, expression => expression.Ignore()) - .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(x => x.ContentTypeAlias, expression => expression.Ignore()) + .ForMember(member => member.Published, expression => expression.Ignore()) + .ForMember(member => member.Updater, expression => expression.Ignore()) + .ForMember(member => member.Trashed, expression => expression.Ignore()) + .ForMember(member => member.Alias, expression => expression.Ignore()) + .ForMember(member => member.ContentTypeAlias, expression => expression.Ignore()) .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()); //FROM IMember TO ContentItemDto config.CreateMap>() - .ForMember( - dto => dto.Owner, - expression => expression.ResolveUsing>()) - .ForMember(x => x.Published, expression => expression.Ignore()) - .ForMember(x => x.Updater, expression => expression.Ignore()) - .ForMember(x => x.Icon, expression => expression.Ignore()) - .ForMember(x => x.Alias, expression => expression.Ignore()) - .ForMember(member => member.HasPublishedVersion, expression => expression.Ignore()) + .ForMember(dto => dto.Owner, expression => expression.ResolveUsing(new OwnerResolver())) + .ForMember(dto => dto.Published, expression => expression.Ignore()) + .ForMember(dto => dto.Updater, expression => expression.Ignore()) + .ForMember(dto => dto.Icon, expression => expression.Ignore()) + .ForMember(dto => dto.Alias, expression => expression.Ignore()) + .ForMember(dto => dto.HasPublishedVersion, expression => expression.Ignore()) //do no map the custom member properties (currently anyways, they were never there in 6.x) - .ForMember(dto => dto.Properties, expression => expression.ResolveUsing()); + .ForMember(dto => dto.Properties, expression => expression.ResolveUsing(new MemberDtoPropertiesValueResolver())); } /// @@ -232,8 +201,28 @@ private static void MapGenericCustomProperties(IMemberService memberService, IMe } }; + TabsAndPropertiesResolver.MapGenericProperties(member, display, localizedText, genericProperties, properties => + { + if (HttpContext.Current != null && UmbracoContext.Current != null && UmbracoContext.Current.Security.CurrentUser != null + && UmbracoContext.Current.Security.CurrentUser.AllowedSections.Any(x => x.Equals(Constants.Applications.Settings))) + { + var memberTypeLink = string.Format("#/member/memberTypes/edit/{0}", member.ContentTypeId); - TabsAndPropertiesResolver.MapGenericProperties(member, display, localizedText, genericProperties); + //Replace the doctype property + var docTypeProperty = properties.First(x => x.Alias == string.Format("{0}doctype", Constants.PropertyEditors.InternalGenericPropertiesPrefix)); + docTypeProperty.Value = new List + { + new + { + linkText = member.ContentType.Name, + url = memberTypeLink, + target = "_self", + icon = "icon-item-arrangement" + } + }; + docTypeProperty.View = "urllist"; + } + }); //check if there's an approval field var provider = membersProvider as global::umbraco.providers.members.UmbracoMembershipProvider; @@ -246,7 +235,6 @@ private static void MapGenericCustomProperties(IMemberService memberService, IMe prop.Value = 1; } } - } /// @@ -255,6 +243,7 @@ private static void MapGenericCustomProperties(IMemberService memberService, IMe /// /// /// + /// /// /// /// If the membership provider installed is the umbraco membership provider, then we will allow changing the username, however if @@ -264,11 +253,11 @@ private static void MapGenericCustomProperties(IMemberService memberService, IMe internal static ContentPropertyDisplay GetLoginProperty(IMemberService memberService, IMember member, MemberDisplay display, ILocalizedTextService localizedText) { var prop = new ContentPropertyDisplay - { - Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), - Label = localizedText.Localize("login"), - Value = display.Username - }; + { + Alias = string.Format("{0}login", Constants.PropertyEditors.InternalGenericPropertiesPrefix), + Label = localizedText.Localize("login"), + Value = display.Username + }; var scenario = memberService.GetMembershipScenario(); @@ -321,8 +310,8 @@ protected override IEnumerable ResolveCore(IMember source) var exclude = defaultProps.Select(x => x.Value.Alias).ToArray(); return source.Properties - .Where(x => exclude.Contains(x.Alias) == false) - .Select(Mapper.Map); + .Where(x => exclude.Contains(x.Alias) == false) + .Select(Mapper.Map); } } @@ -375,7 +364,7 @@ protected override IEnumerable> ResolveCore(IContent } else { - var umbracoProvider = (IUmbracoMemberTypeMembershipProvider)provider; + var umbracoProvider = (IUmbracoMemberTypeMembershipProvider) provider; //This is kind of a hack because a developer is supposed to be allowed to set their property editor - would have been much easier // if we just had all of the membeship provider fields on the member table :( @@ -389,8 +378,6 @@ protected override IEnumerable> ResolveCore(IContent return result; } - - } } @@ -413,8 +400,8 @@ protected override MembershipScenario ResolveCore(IMember source) } var memberType = _memberTypeService.Value.Get(Constants.Conventions.MemberTypes.DefaultAlias); return memberType != null - ? MembershipScenario.CustomProviderWithUmbracoLink - : MembershipScenario.StandaloneCustomProvider; + ? MembershipScenario.CustomProviderWithUmbracoLink + : MembershipScenario.StandaloneCustomProvider; } } @@ -438,7 +425,7 @@ protected override IDictionary ResolveCore(IMember source) } else { - var umbracoProvider = (IUmbracoMemberTypeMembershipProvider)provider; + var umbracoProvider = (IUmbracoMemberTypeMembershipProvider) provider; return new Dictionary { @@ -447,10 +434,7 @@ protected override IDictionary ResolveCore(IMember source) {Constants.Conventions.Member.Comments, umbracoProvider.CommentPropertyTypeAlias} }; } - - } } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Profiling/StartupWebProfilerProvider.cs b/src/Umbraco.Web/Profiling/StartupWebProfilerProvider.cs deleted file mode 100644 index 72a398f17f64..000000000000 --- a/src/Umbraco.Web/Profiling/StartupWebProfilerProvider.cs +++ /dev/null @@ -1,159 +0,0 @@ -using System.Threading; -using System.Web; -using StackExchange.Profiling; -using Umbraco.Core; - -namespace Umbraco.Web.Profiling -{ - /// - /// This is a custom MiniProfiler WebRequestProfilerProvider (which is generally the default) that allows - /// us to profile items during app startup - before an HttpRequest is created - /// - /// - /// Once the boot phase is changed to StartupPhase.Request then the base class (default) provider will handle all - /// profiling data and this sub class no longer performs any logic. - /// - internal class StartupWebProfilerProvider : WebRequestProfilerProvider - { - public StartupWebProfilerProvider() - { - _startupPhase = StartupPhase.Boot; - //create the startup profiler - _startupProfiler = new MiniProfiler("http://localhost/umbraco-startup", ProfileLevel.Verbose) - { - Name = "StartupProfiler" - }; - } - - private MiniProfiler _startupProfiler; - private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); - - /// - /// Used to determine which phase the boot process is in - /// - private enum StartupPhase - { - None = 0, - Boot = 1, - Request = 2 - } - - private volatile StartupPhase _startupPhase; - - /// - /// Executed once the application boot process is complete and changes the phase to Request - /// - public void BootComplete() - { - using (new ReadLock(_locker)) - { - if (_startupPhase != StartupPhase.Boot) return; - } - - using (var l = new UpgradeableReadLock(_locker)) - { - if (_startupPhase == StartupPhase.Boot) - { - l.UpgradeToWriteLock(); - _startupPhase = StartupPhase.Request; - } - } - } - - /// - /// Executed when a profiling operation is completed - /// - /// - /// - /// This checks if the bootup phase is None, if so, it just calls the base class, otherwise it checks - /// if a profiler is active (i.e. in startup), then sets the phase to None so that the base class will be used - /// for all subsequent calls. - /// - public override void Stop(bool discardResults) - { - using (new ReadLock(_locker)) - { - if (_startupPhase == StartupPhase.None) - { - base.Stop(discardResults); - return; - } - } - - using (var l = new UpgradeableReadLock(_locker)) - { - if (_startupPhase > 0 && base.GetCurrentProfiler() == null) - { - l.UpgradeToWriteLock(); - - _startupPhase = StartupPhase.None; - - //This is required to pass the mini profiling context from before a request - // to the current startup request. - if (HttpContext.Current != null) - { - HttpContext.Current.Items[":mini-profiler:"] = _startupProfiler; - base.Stop(discardResults); - _startupProfiler = null; - } - } - else - { - base.Stop(discardResults); - } - } - } - - /// - /// Executed when a profiling operation is started - /// - /// - /// - /// - /// This checks if the startup phase is not None, if this is the case and the current profiler is NULL - /// then this sets the startup profiler to be active. Otherwise it just calls the base class Start method. - /// - public override MiniProfiler Start(ProfileLevel level) - { - using (new ReadLock(_locker)) - { - if (_startupPhase > 0 && base.GetCurrentProfiler() == null) - { - SetProfilerActive(_startupProfiler); - return _startupProfiler; - } - - return base.Start(level); - } - } - - /// - /// This returns the current profiler - /// - /// - /// - /// If the boot phase is not None, then this will return the startup profiler (this), otherwise - /// returns the base class - /// - public override MiniProfiler GetCurrentProfiler() - { - using (new ReadLock(_locker)) - { - if (_startupPhase > 0) - { - try - { - var current = base.GetCurrentProfiler(); - if (current == null) return _startupProfiler; - } - catch - { - return _startupProfiler; - } - } - - return base.GetCurrentProfiler(); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Profiling/WebProfiler.cs b/src/Umbraco.Web/Profiling/WebProfiler.cs index a1998c8761bb..62d69019d63d 100644 --- a/src/Umbraco.Web/Profiling/WebProfiler.cs +++ b/src/Umbraco.Web/Profiling/WebProfiler.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Web; using StackExchange.Profiling; using StackExchange.Profiling.SqlFormatters; @@ -14,22 +15,23 @@ namespace Umbraco.Web.Profiling /// internal class WebProfiler : IProfiler { - private StartupWebProfilerProvider _startupWebProfilerProvider; + private const string BootRequestItemKey = "Umbraco.Web.Profiling.WebProfiler__isBootRequest"; + private WebProfilerProvider _provider; + private int _first; /// /// Constructor /// internal WebProfiler() { - //setup some defaults + // create our own provider, which can provide a profiler even during boot + // MiniProfiler's default cannot because there's no HttpRequest in HttpContext + _provider = new WebProfilerProvider(); + + // settings MiniProfiler.Settings.SqlFormatter = new SqlServerFormatter(); MiniProfiler.Settings.StackMaxLength = 5000; - - //At this point we know that we've been constructed during app startup, there won't be an HttpRequest in the HttpContext - // since it hasn't started yet. So we need to do some hacking to enable profiling during startup. - _startupWebProfilerProvider = new StartupWebProfilerProvider(); - //this should always be the case during startup, we'll need to set a custom profiler provider - MiniProfiler.Settings.ProfilerProvider = _startupWebProfilerProvider; + MiniProfiler.Settings.ProfilerProvider = _provider; //Binds to application events to enable the MiniProfiler with a real HttpRequest UmbracoApplicationBase.ApplicationInit += UmbracoApplicationApplicationInit; @@ -57,43 +59,33 @@ void UmbracoApplicationApplicationInit(object sender, EventArgs e) } } - /// - /// Handle the begin request event - /// - /// - /// - void UmbracoApplicationEndRequest(object sender, EventArgs e) + void UmbracoApplicationBeginRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) + // if this is the first request, notify our own provider that this request is the boot request + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first) { - Stop(); - _startupWebProfilerProvider = null; - } - else if (CanPerformProfilingAction(sender)) - { - Stop(); + _provider.BeginBootRequest(); + ((HttpApplication)sender).Context.Items[BootRequestItemKey] = true; + // and no need to start anything, profiler is already there } + // else start a profiler, the normal way + else if (ShouldProfile(sender)) + Start(); } - /// - /// Handle the end request event - /// - /// - /// - void UmbracoApplicationBeginRequest(object sender, EventArgs e) + void UmbracoApplicationEndRequest(object sender, EventArgs e) { - if (_startupWebProfilerProvider != null) - { - _startupWebProfilerProvider.BootComplete(); - } - - if (CanPerformProfilingAction(sender)) - { - Start(); - } + // if this is the boot request, or if we should profile this request, stop + // (the boot request is always profiled, no matter what) + var isBootRequest = ((HttpApplication)sender).Context.Items[BootRequestItemKey] != null; // fixme perfs + if (isBootRequest) + _provider.EndBootRequest(); + if (isBootRequest || ShouldProfile(sender)) + Stop(); } - private bool CanPerformProfilingAction(object sender) + private bool ShouldProfile(object sender) { if (GlobalSettings.DebugMode == false) return false; @@ -108,10 +100,10 @@ private bool CanPerformProfilingAction(object sender) return false; if (string.IsNullOrEmpty(request.Result.QueryString["umbDebug"])) - return true; + return false; if (request.Result.Url.IsBackOfficeRequest(HttpRuntime.AppDomainAppVirtualPath)) - return true; + return false; return true; } diff --git a/src/Umbraco.Web/Profiling/WebProfilerProvider.cs b/src/Umbraco.Web/Profiling/WebProfilerProvider.cs new file mode 100644 index 000000000000..ffd1871ecc0a --- /dev/null +++ b/src/Umbraco.Web/Profiling/WebProfilerProvider.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading; +using System.Web; +using StackExchange.Profiling; + +namespace Umbraco.Web.Profiling +{ + /// + /// This is a custom MiniProfiler WebRequestProfilerProvider (which is generally the default) that allows + /// us to profile items during app startup - before an HttpRequest is created + /// + /// + /// Once the boot phase is changed to StartupPhase.Request then the base class (default) provider will handle all + /// profiling data and this sub class no longer performs any logic. + /// + internal class WebProfilerProvider : WebRequestProfilerProvider + { + private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); + private MiniProfiler _startupProfiler; + private int _first; + private volatile BootPhase _bootPhase; + + public WebProfilerProvider() + { + // booting... + _bootPhase = BootPhase.Boot; + } + + /// + /// Indicates the boot phase. + /// + private enum BootPhase + { + Boot = 0, // boot phase (before the 1st request even begins) + BootRequest = 1, // request boot phase (during the 1st request) + Booted = 2 // done booting + } + + public void BeginBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.Boot) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.BootRequest; + + // assign the profiler to be the current MiniProfiler for the request + // is's already active, starting and all + HttpContext.Current.Items[":mini-profiler:"] = _startupProfiler; + } + finally + { + _locker.ExitWriteLock(); + } + } + + public void EndBootRequest() + { + _locker.EnterWriteLock(); + try + { + if (_bootPhase != BootPhase.BootRequest) + throw new InvalidOperationException("Invalid boot phase."); + _bootPhase = BootPhase.Booted; + + _startupProfiler = null; + } + finally + { + _locker.ExitWriteLock(); + } + } + + /// + /// Executed when a profiling operation is started + /// + /// + /// + /// + /// This checks if the startup phase is not None, if this is the case and the current profiler is NULL + /// then this sets the startup profiler to be active. Otherwise it just calls the base class Start method. + /// + public override MiniProfiler Start(ProfileLevel level) + { + var first = Interlocked.Exchange(ref _first, 1) == 0; + if (first == false) return base.Start(level); + + _startupProfiler = new MiniProfiler("http://localhost/umbraco-startup") { Name = "StartupProfiler" }; + SetProfilerActive(_startupProfiler); + return _startupProfiler; + } + + /// + /// This returns the current profiler + /// + /// + /// + /// If the boot phase is not None, then this will return the startup profiler (this), otherwise + /// returns the base class + /// + public override MiniProfiler GetCurrentProfiler() + { + // if not booting then just use base (fast) + // no lock, _bootPhase is volatile + if (_bootPhase == BootPhase.Booted) + return base.GetCurrentProfiler(); + + // else + try + { + var current = base.GetCurrentProfiler(); + return current ?? _startupProfiler; + } + catch + { + return _startupProfiler; + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs new file mode 100644 index 000000000000..1fb9b267ab05 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ParameterEditors/MultipleContentPickerParameterEditor.cs @@ -0,0 +1,14 @@ +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors.ParameterEditors +{ + [ParameterEditor(Constants.PropertyEditors.MultiNodeTreePickerAlias, "Multiple Content Picker", "contentpicker")] + public class MultipleContentPickerParameterEditor : ParameterEditor + { + public MultipleContentPickerParameterEditor() + { + Configuration.Add("multiPicker", "1"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs index b44c65b1577c..be228bf3efe3 100644 --- a/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/TagsPropertyEditor.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Net; using System.Runtime.InteropServices; using Newtonsoft.Json.Linq; using Umbraco.Core; @@ -60,7 +61,14 @@ public TagPropertyValueEditor(PropertyValueEditor wrapped) public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { var json = editorValue.Value as JArray; - return json == null ? null : json.Select(x => x.Value()); + return json == null + ? null + : json.Select(x => x.Value()).Where(x => x.IsNullOrWhiteSpace() == false) + //First we will decode it as html because we know that if this is not a malicious post that the value is + // already Html encoded by the tags JavaScript controller. Then we'll re-Html Encode it to ensure that in case this + // is a malicious post (i.e. someone is submitting data manually by modifying the request). + .Select(WebUtility.HtmlDecode) + .Select(WebUtility.HtmlEncode); } /// diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs index 884138a9b426..e58bc73b254e 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedContentCache.cs @@ -16,7 +16,9 @@ using System.Linq; using umbraco.BusinessLogic; using umbraco.presentation.preview; +using Umbraco.Core.Services; using GlobalSettings = umbraco.GlobalSettings; +using Task = System.Threading.Tasks.Task; namespace Umbraco.Web.PublishedCache.XmlPublishedCache { @@ -26,6 +28,13 @@ internal class PublishedContentCache : IPublishedContentCache private readonly RoutesCache _routesCache = new RoutesCache(!UnitTesting); + private DomainHelper _domainHelper; + + private DomainHelper GetDomainHelper(IDomainService domainService) + { + return _domainHelper ?? (_domainHelper = new DomainHelper(domainService)); + } + // for INTERNAL, UNIT TESTS use ONLY internal RoutesCache RoutesCache { get { return _routesCache; } } @@ -99,6 +108,13 @@ public virtual string GetRouteById(UmbracoContext umbracoContext, bool preview, // - non-colliding, adds one complete "by route" lookup, only on the first time a url is computed (then it's cached anyways) // - colliding, adds one "by route" lookup, the first time the url is computed, then one dictionary looked each time it is computed again // assuming no collisions, the impact is one complete "by route" lookup the first time each url is computed + // + // U4-9121 - this lookup is too expensive when computing a large amount of urls on a front-end (eg menu) + // ... thinking about moving the lookup out of the path into its own async task, so we are not reporting errors + // in the back-office anymore, but at least we are not polluting the cache + // instead, refactored DeterminedIdByRoute to stop using XPath, with a 16x improvement according to benchmarks + // will it be enough? + var loopId = preview ? 0 : _routesCache.GetNodeId(route); // might be cached already in case of collision if (loopId == 0) { @@ -130,62 +146,141 @@ IPublishedContent DetermineIdByRoute(UmbracoContext umbracoContext, bool preview var pos = route.IndexOf('/'); var path = pos == 0 ? route : route.Substring(pos); var startNodeId = pos == 0 ? 0 : int.Parse(route.Substring(0, pos)); - IEnumerable vars; - - var xpath = CreateXpathQuery(startNodeId, path, hideTopLevelNode, out vars); //check if we can find the node in our xml cache - var content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray()); + var id = NavigateRoute(umbracoContext, preview, startNodeId, path, hideTopLevelNode); + if (id > 0) return GetById(umbracoContext, preview, id); // if hideTopLevelNodePath is true then for url /foo we looked for /*/foo // but maybe that was the url of a non-default top-level node, so we also // have to look for /foo (see note in ApplyHideTopLevelNodeFromPath). - if (content == null && hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0) + if (hideTopLevelNode && path.Length > 1 && path.IndexOf('/', 1) < 0) { - xpath = CreateXpathQuery(startNodeId, path, false, out vars); - content = GetSingleByXPath(umbracoContext, preview, xpath, vars == null ? null : vars.ToArray()); + var id2 = NavigateRoute(umbracoContext, preview, startNodeId, path, false); + if (id2 > 0) return GetById(umbracoContext, preview, id2); } - return content; + return null; + } + + private int NavigateRoute(UmbracoContext umbracoContext, bool preview, int startNodeId, string path, bool hideTopLevelNode) + { + var xml = GetXml(umbracoContext, preview); + XmlElement elt; + + // empty path + if (path == string.Empty || path == "/") + { + if (startNodeId > 0) + { + elt = xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture)); + return elt == null ? -1 : startNodeId; + } + + elt = null; + var min = int.MaxValue; + foreach (var e in xml.DocumentElement.ChildNodes.OfType()) + { + var sortOrder = int.Parse(e.GetAttribute("sortOrder")); + if (sortOrder < min) + { + min = sortOrder; + elt = e; + } + } + return elt == null ? -1 : int.Parse(elt.GetAttribute("id")); + } + + // non-empty path + elt = startNodeId <= 0 + ? xml.DocumentElement + : xml.GetElementById(startNodeId.ToString(CultureInfo.InvariantCulture)); + if (elt == null) return -1; + + var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries); + + if (hideTopLevelNode && startNodeId <= 0) + { + foreach (var e in elt.ChildNodes.OfType()) + { + var id = NavigateElementRoute(e, urlParts); + if (id > 0) return id; + } + return -1; + } + + return NavigateElementRoute(elt, urlParts); + } + + private static bool UseLegacySchema + { + get { return UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema; } + } + + private static int NavigateElementRoute(XmlElement elt, string[] urlParts) + { + var found = true; + var i = 0; + while (found && i < urlParts.Length) + { + found = false; + foreach (var child in elt.ChildNodes.OfType()) + { + var noNode = UseLegacySchema + ? child.Name != "node" + : child.GetAttributeNode("isDoc") == null; + if (noNode) continue; + if (child.GetAttribute("urlName") != urlParts[i]) continue; + + found = true; + elt = child; + break; + } + i++; + } + return found ? int.Parse(elt.GetAttribute("id")) : -1; } string DetermineRouteById(UmbracoContext umbracoContext, bool preview, int contentId) { - var node = GetById(umbracoContext, preview, contentId); - if (node == null) - return null; + var elt = GetXml(umbracoContext, preview).GetElementById(contentId.ToString(CultureInfo.InvariantCulture)); + if (elt == null) return null; - var domainHelper = new DomainHelper(umbracoContext.Application.Services.DomainService); + var domainHelper = GetDomainHelper(umbracoContext.Application.Services.DomainService); // walk up from that node until we hit a node with a domain, // or we reach the content root, collecting urls in the way var pathParts = new List(); - var n = node; - var hasDomains = domainHelper.NodeHasDomains(n.Id); - while (hasDomains == false && n != null) // n is null at root + var eltId = int.Parse(elt.GetAttribute("id")); + var eltParentId = int.Parse(((XmlElement) elt.ParentNode).GetAttribute("id")); + var e = elt; + var id = eltId; + var hasDomains = domainHelper.NodeHasDomains(id); + while (hasDomains == false && id != -1) { // get the url - var urlName = n.UrlName; + var urlName = e.GetAttribute("urlName"); pathParts.Add(urlName); // move to parent node - n = n.Parent; - hasDomains = n != null && domainHelper.NodeHasDomains(n.Id); + e = (XmlElement) e.ParentNode; + id = int.Parse(e.GetAttribute("id")); + hasDomains = id != -1 && domainHelper.NodeHasDomains(id); } // no domain, respect HideTopLevelNodeFromPath for legacy purposes - if (hasDomains == false && global::umbraco.GlobalSettings.HideTopLevelNodeFromPath) - ApplyHideTopLevelNodeFromPath(umbracoContext, node, pathParts); + if (hasDomains == false && GlobalSettings.HideTopLevelNodeFromPath) + ApplyHideTopLevelNodeFromPath(umbracoContext, eltId, eltParentId, pathParts); // assemble the route pathParts.Reverse(); var path = "/" + string.Join("/", pathParts); // will be "/" or "/foo" or "/foo/bar" etc - var route = (n == null ? "" : n.Id.ToString(CultureInfo.InvariantCulture)) + path; + var route = (id == -1 ? "" : id.ToString(CultureInfo.InvariantCulture)) + path; return route; } - static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublishedContent node, IList pathParts) + static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, int nodeId, int parentId, IList pathParts) { // in theory if hideTopLevelNodeFromPath is true, then there should be only once // top-level node, or else domains should be assigned. but for backward compatibility @@ -195,12 +290,12 @@ static void ApplyHideTopLevelNodeFromPath(UmbracoContext umbracoContext, IPublis // "/foo" fails (looking for "/*/foo") we try also "/foo". // this does not make much sense anyway esp. if both "/foo/" and "/bar/foo" exist, but // that's the way it works pre-4.10 and we try to be backward compat for the time being - if (node.Parent == null) + if (parentId == -1) { var rootNode = umbracoContext.ContentCache.GetByRoute("/", true); if (rootNode == null) throw new Exception("Failed to get node at /."); - if (rootNode.Id == node.Id) // remove only if we're the default node + if (rootNode.Id == nodeId) // remove only if we're the default node pathParts.RemoveAt(pathParts.Count - 1); } else @@ -217,12 +312,7 @@ class XPathStringsDefinition { public int Version { get; private set; } - public static string Root { get { return "/root"; } } public string RootDocuments { get; private set; } - public string DescendantDocumentById { get; private set; } - public string ChildDocumentByUrlName { get; private set; } - public string ChildDocumentByUrlNameVar { get; private set; } - public string RootDocumentWithLowestSortOrder { get; private set; } public XPathStringsDefinition(int version) { @@ -233,19 +323,11 @@ public XPathStringsDefinition(int version) // legacy XML schema case 0: RootDocuments = "/root/node"; - DescendantDocumentById = "//node [@id={0}]"; - ChildDocumentByUrlName = "/node [@urlName='{0}']"; - ChildDocumentByUrlNameVar = "/node [@urlName=${0}]"; - RootDocumentWithLowestSortOrder = "/root/node [not(@sortOrder > ../node/@sortOrder)][1]"; break; // default XML schema as of 4.10 case 1: RootDocuments = "/root/* [@isDoc]"; - DescendantDocumentById = "//* [@isDoc and @id={0}]"; - ChildDocumentByUrlName = "/* [@isDoc and @urlName='{0}']"; - ChildDocumentByUrlNameVar = "/* [@isDoc and @urlName=${0}]"; - RootDocumentWithLowestSortOrder = "/root/* [@isDoc and not(@sortOrder > ../* [@isDoc]/@sortOrder)][1]"; break; default: @@ -421,84 +503,6 @@ internal XmlDocument GetXml(UmbracoContext umbracoContext, bool preview) static readonly char[] SlashChar = new[] { '/' }; - protected string CreateXpathQuery(int startNodeId, string path, bool hideTopLevelNodeFromPath, out IEnumerable vars) - { - string xpath; - vars = null; - - if (path == string.Empty || path == "/") - { - // if url is empty - if (startNodeId > 0) - { - // if in a domain then use the root node of the domain - xpath = string.Format(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); - } - else - { - // if not in a domain - what is the default page? - // let's say it is the first one in the tree, if any -- order by sortOrder - - // but! - // umbraco does not consistently guarantee that sortOrder starts with 0 - // so the one that we want is the one with the smallest sortOrder - // read http://stackoverflow.com/questions/1128745/how-can-i-use-xpath-to-find-the-minimum-value-of-an-attribute-in-a-set-of-elemen - - // so that one does not work, because min(@sortOrder) maybe 1 - // xpath = "/root/*[@isDoc and @sortOrder='0']"; - - // and we can't use min() because that's XPath 2.0 - // that one works - xpath = XPathStrings.RootDocumentWithLowestSortOrder; - } - } - else - { - // if url is not empty, then use it to try lookup a matching page - var urlParts = path.Split(SlashChar, StringSplitOptions.RemoveEmptyEntries); - var xpathBuilder = new StringBuilder(); - int partsIndex = 0; - List varsList = null; - - if (startNodeId == 0) - { - if (hideTopLevelNodeFromPath) - xpathBuilder.Append(XPathStrings.RootDocuments); // first node is not in the url - else - xpathBuilder.Append(XPathStringsDefinition.Root); - } - else - { - xpathBuilder.AppendFormat(XPathStringsDefinition.Root + XPathStrings.DescendantDocumentById, startNodeId); - // always "hide top level" when there's a domain - } - - while (partsIndex < urlParts.Length) - { - var part = urlParts[partsIndex++]; - if (part.Contains('\'') || part.Contains('"')) - { - // use vars, escaping gets ugly pretty quickly - varsList = varsList ?? new List(); - var varName = string.Format("var{0}", partsIndex); - varsList.Add(new XPathVariable(varName, part)); - xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlNameVar, varName); - } - else - { - xpathBuilder.AppendFormat(XPathStrings.ChildDocumentByUrlName, part); - - } - } - - xpath = xpathBuilder.ToString(); - if (varsList != null) - vars = varsList.ToArray(); - } - - return xpath; - } - #endregion #region Detached diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index da1ff94fe1d8..f3cc101b2162 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -226,7 +226,7 @@ private CacheValues GetUmbracoMediaCacheValues(int id) // the media from the service, first var media = ApplicationContext.Current.Services.MediaService.GetById(id); - if (media == null) return null; // not found, ok + if (media == null || media.Trashed) return null; // not found, ok // so, the media was not found in Examine's index *yet* it exists, which probably indicates that // the index is corrupted. Or not up-to-date. Log a warning, but only once, and only if seeing the diff --git a/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs b/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs index fd09f5ff8c2a..3eefbc4d0c65 100644 --- a/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs +++ b/src/Umbraco.Web/Routing/EnsureRoutableOutcome.cs @@ -3,7 +3,7 @@ namespace Umbraco.Web.Routing /// /// Represents the outcome of trying to route an incoming request. /// - internal enum EnsureRoutableOutcome + public enum EnsureRoutableOutcome { /// /// Request routes to a document. diff --git a/src/Umbraco.Web/Routing/RoutableAttemptEventArgs.cs b/src/Umbraco.Web/Routing/RoutableAttemptEventArgs.cs index 7a7bc37d5cf6..ee936232521f 100644 --- a/src/Umbraco.Web/Routing/RoutableAttemptEventArgs.cs +++ b/src/Umbraco.Web/Routing/RoutableAttemptEventArgs.cs @@ -5,7 +5,7 @@ namespace Umbraco.Web.Routing /// /// Event args containing information about why the request was not routable, or if it is routable /// - internal class RoutableAttemptEventArgs : UmbracoRequestEventArgs + public class RoutableAttemptEventArgs : UmbracoRequestEventArgs { public EnsureRoutableOutcome Outcome { get; private set; } diff --git a/src/Umbraco.Web/Routing/UmbracoRequestEventArgs.cs b/src/Umbraco.Web/Routing/UmbracoRequestEventArgs.cs index 2c50c972f589..35162772759c 100644 --- a/src/Umbraco.Web/Routing/UmbracoRequestEventArgs.cs +++ b/src/Umbraco.Web/Routing/UmbracoRequestEventArgs.cs @@ -6,7 +6,7 @@ namespace Umbraco.Web.Routing /// /// Event args used for event launched during a request (like in the UmbracoModule) /// - internal class UmbracoRequestEventArgs : EventArgs + public class UmbracoRequestEventArgs : EventArgs { public UmbracoContext UmbracoContext { get; private set; } public HttpContextBase HttpContext { get; private set; } diff --git a/src/Umbraco.Web/Search/ExamineEvents.cs b/src/Umbraco.Web/Search/ExamineEvents.cs index 20701fe33027..7fbbf29b89f7 100644 --- a/src/Umbraco.Web/Search/ExamineEvents.cs +++ b/src/Umbraco.Web/Search/ExamineEvents.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Xml; @@ -67,12 +68,12 @@ protected override void ApplicationStarted(UmbracoApplicationBase httpApplicatio /// /// This is used to refresh content indexers IndexData based on the DataService whenever a content type is changed since - /// properties may have been added/removed + /// properties may have been added/removed, then we need to re-index any required data if aliases have been changed /// /// /// /// - /// See: http://issues.umbraco.org/issue/U4-4798 + /// See: http://issues.umbraco.org/issue/U4-4798, http://issues.umbraco.org/issue/U4-7833 /// static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs e) { @@ -81,6 +82,88 @@ static void ContentTypeCacheRefresherCacheUpdated(ContentTypeCacheRefresher send { provider.RefreshIndexerDataFromDataService(); } + + if (e.MessageType == MessageType.RefreshByJson) + { + var contentTypesChanged = new HashSet(); + var mediaTypesChanged = new HashSet(); + var memberTypesChanged = new HashSet(); + + var payloads = ContentTypeCacheRefresher.DeserializeFromJsonPayload(e.MessageObject.ToString()); + foreach (var payload in payloads) + { + if (payload.IsNew == false + && (payload.WasDeleted || payload.AliasChanged || payload.PropertyRemoved || payload.PropertyTypeAliasChanged)) + { + //if we get here it means that some aliases have changed and the indexes for those particular doc types will need to be updated + if (payload.Type == typeof(IContentType).Name) + { + //if it is content + contentTypesChanged.Add(payload.Alias); + } + else if (payload.Type == typeof(IMediaType).Name) + { + //if it is media + mediaTypesChanged.Add(payload.Alias); + } + else if (payload.Type == typeof(IMemberType).Name) + { + //if it is members + memberTypesChanged.Add(payload.Alias); + } + } + } + + //TODO: We need to update Examine to support re-indexing multiple items at once instead of one by one which will speed up + // the re-indexing process, we don't want to revert to rebuilding the whole thing! + + if (contentTypesChanged.Count > 0) + { + foreach (var alias in contentTypesChanged) + { + var ctType = ApplicationContext.Current.Services.ContentTypeService.GetContentType(alias); + if (ctType != null) + { + var contentItems = ApplicationContext.Current.Services.ContentService.GetContentOfContentType(ctType.Id); + foreach (var contentItem in contentItems) + { + ReIndexForContent(contentItem, contentItem.HasPublishedVersion && contentItem.Trashed == false); + } + } + } + } + if (mediaTypesChanged.Count > 0) + { + foreach (var alias in mediaTypesChanged) + { + var ctType = ApplicationContext.Current.Services.ContentTypeService.GetMediaType(alias); + if (ctType != null) + { + var mediaItems = ApplicationContext.Current.Services.MediaService.GetMediaOfMediaType(ctType.Id); + foreach (var mediaItem in mediaItems) + { + ReIndexForMedia(mediaItem, mediaItem.Trashed == false); + } + } + } + } + if (memberTypesChanged.Count > 0) + { + foreach (var alias in memberTypesChanged) + { + var ctType = ApplicationContext.Current.Services.MemberTypeService.Get(alias); + if (ctType != null) + { + var memberItems = ApplicationContext.Current.Services.MemberService.GetMembersByMemberType(ctType.Id); + foreach (var memberItem in memberItems) + { + ReIndexForMember(memberItem); + } + } + } + } + } + } static void MemberCacheRefresherCacheUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs e) @@ -432,7 +515,7 @@ private static void ReIndexForContent(IContent sender, bool isContentPublished) //add an icon attribute to get indexed xml.Add(new XAttribute("icon", sender.ContentType.Icon)); - ExamineManager.Instance.ReIndexNode( + ExamineManager.Instance.ReIndexNode( xml, IndexTypes.Content, ExamineManager.Instance.IndexProviderCollection.OfType() diff --git a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs index 3cc7908dfdb2..34ad71d2f938 100644 --- a/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs +++ b/src/Umbraco.Web/Search/LuceneIndexerExtensions.cs @@ -5,6 +5,8 @@ using Examine.Providers; using Lucene.Net.Index; using Lucene.Net.Search; +using Lucene.Net.Store; +using Umbraco.Core.Logging; namespace Umbraco.Web.Search { @@ -21,9 +23,17 @@ internal static class ExamineExtensions /// public static int GetIndexDocumentCount(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + try { - return reader.NumDocs(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.NumDocs(); + } + } + catch (AlreadyClosedException) + { + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get GetIndexDocumentCount, the writer is already closed"); + return 0; } } @@ -34,9 +44,19 @@ public static int GetIndexDocumentCount(this LuceneIndexer indexer) /// public static int GetIndexFieldCount(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + //TODO: check for closing! and AlreadyClosedException + + try + { + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.GetFieldNames(IndexReader.FieldOption.ALL).Count; + } + } + catch (AlreadyClosedException) { - return reader.GetFieldNames(IndexReader.FieldOption.ALL).Count; + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get GetIndexFieldCount, the writer is already closed"); + return 0; } } @@ -47,9 +67,17 @@ public static int GetIndexFieldCount(this LuceneIndexer indexer) /// public static bool IsIndexOptimized(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + try { - return reader.IsOptimized(); + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.IsOptimized(); + } + } + catch (AlreadyClosedException) + { + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get IsIndexOptimized, the writer is already closed"); + return false; } } @@ -74,9 +102,17 @@ public static bool IsIndexLocked(this LuceneIndexer indexer) /// public static int GetDeletedDocumentsCount(this LuceneIndexer indexer) { - using (var reader = indexer.GetIndexWriter().GetReader()) + try + { + using (var reader = indexer.GetIndexWriter().GetReader()) + { + return reader.NumDeletedDocs(); + } + } + catch (AlreadyClosedException) { - return reader.NumDeletedDocs(); + LogHelper.Warn(typeof(ExamineExtensions), "Cannot get GetDeletedDocumentsCount, the writer is already closed"); + return 0; } } } diff --git a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs deleted file mode 100644 index c3920677c520..000000000000 --- a/src/Umbraco.Web/Strategies/Migrations/RebuildMediaXmlCacheAfterUpgrade.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using Umbraco.Core; -using Umbraco.Core.Events; -using Umbraco.Core.Persistence.Migrations; -using Umbraco.Core.Services; -using umbraco.interfaces; -using Umbraco.Core.Configuration; - -namespace Umbraco.Web.Strategies.Migrations -{ - /// - /// This will execute after upgrading to rebuild the xml cache - /// - /// - /// This cannot execute as part of a db migration since we need access to the services/repos. - /// - /// This will execute for specific versions - - /// - /// * If current is less than or equal to 7.0.0 - /// - public class RebuildMediaXmlCacheAfterUpgrade : MigrationStartupHander - { - protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) - { - if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; - - var target70 = new Version(7, 0, 0); - - if (e.ConfiguredVersion <= target70) - { - var mediasvc = (MediaService)ApplicationContext.Current.Services.MediaService; - mediasvc.RebuildXmlStructures(); - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs b/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs new file mode 100644 index 000000000000..e62a7386753e --- /dev/null +++ b/src/Umbraco.Web/Strategies/Migrations/RebuildXmlCachesAfterUpgrade.cs @@ -0,0 +1,52 @@ +using System; +using umbraco; +using Umbraco.Core; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence.Migrations; +using Umbraco.Core.Services; +using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings; + +namespace Umbraco.Web.Strategies.Migrations +{ + /// + /// Rebuilds the Xml caches after upgrading. + /// This will execute after upgrading to rebuild the xml cache + /// + /// + /// This cannot execute as part of a DB migration since it needs access to services and repositories. + /// Executes for: + /// - Media Xml : if current is less than, or equal to, 7.0.0 (superceeded by the next rule) + /// - Media & Content Xml : if current is less than, or equal to, 7.3.0 - because 7.3.0 adds .Key to cached items + /// + /// + public class RebuildXmlCachesAfterUpgrade : MigrationStartupHander + { + protected override void AfterMigration(MigrationRunner sender, MigrationEventArgs e) + { + if (e.ProductName != GlobalSettings.UmbracoMigrationName) return; + + var v730 = new Semver.SemVersion(new Version(7, 3, 0)); + + var doMedia = e.ConfiguredSemVersion < v730; + var doContent = e.ConfiguredSemVersion < v730; + + if (doMedia) + { + var mediaService = (MediaService) ApplicationContext.Current.Services.MediaService; + mediaService.RebuildXmlStructures(); + + // note: not re-indexing medias? + } + + if (doContent) + { + // rebuild Xml in database + var contentService = (ContentService) ApplicationContext.Current.Services.ContentService; + contentService.RebuildXmlStructures(); + + // refresh the Xml cache + content.Instance.RefreshContentFromDatabase(); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Templates/TemplateUtilities.cs b/src/Umbraco.Web/Templates/TemplateUtilities.cs index 881cb563c537..c56d7b5b8ae8 100644 --- a/src/Umbraco.Web/Templates/TemplateUtilities.cs +++ b/src/Umbraco.Web/Templates/TemplateUtilities.cs @@ -7,17 +7,17 @@ namespace Umbraco.Web.Templates { - //NOTE: I realize there is only one class in this namespace but I'm pretty positive that there will be more classes in - //this namespace once we start migrating and cleaning up more code. + //NOTE: I realize there is only one class in this namespace but I'm pretty positive that there will be more classes in + //this namespace once we start migrating and cleaning up more code. - /// - /// Utility class used for templates - /// - public static class TemplateUtilities - { + /// + /// Utility class used for templates + /// + public static class TemplateUtilities + { //TODO: Pass in an Umbraco context!!!!!!!! Don't rely on the singleton so things are more testable internal static string ParseInternalLinks(string text, bool preview) - { + { // save and set for url provider var inPreviewMode = UmbracoContext.Current.InPreviewMode; UmbracoContext.Current.InPreviewMode = preview; @@ -33,79 +33,84 @@ internal static string ParseInternalLinks(string text, bool preview) } return text; - } + } - /// - /// Parses the string looking for the {localLink} syntax and updates them to their correct links. - /// - /// - /// - public static string ParseInternalLinks(string text) - { + /// + /// Parses the string looking for the {localLink} syntax and updates them to their correct links. + /// + /// + /// + public static string ParseInternalLinks(string text) + { //TODO: Pass in an Umbraco context!!!!!!!! Don't rely on the singleton so things are more testable, better yet, pass in urlprovider, routing context, separately - //don't attempt to proceed without a context as we cannot lookup urls without one - if (UmbracoContext.Current == null || UmbracoContext.Current.RoutingContext == null) - { - return text; - } + //don't attempt to proceed without a context as we cannot lookup urls without one + if (UmbracoContext.Current == null || UmbracoContext.Current.RoutingContext == null) + { + return text; + } - var urlProvider = UmbracoContext.Current.UrlProvider; + var urlProvider = UmbracoContext.Current.UrlProvider; - // Parse internal links - var tags = Regex.Matches(text, @"href=""[/]?(?:\{|\%7B)localLink:([0-9]+)(?:\}|\%7D)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - foreach (Match tag in tags) - if (tag.Groups.Count > 0) - { - var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); - var newLink = urlProvider.GetUrl(int.Parse(id)); - text = text.Replace(tag.Value, "href=\"" + newLink); - } + // Parse internal links + var tags = Regex.Matches(text, @"href=""[/]?(?:\{|\%7B)localLink:([0-9]+)(?:\}|\%7D)", RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + foreach (Match tag in tags) + if (tag.Groups.Count > 0) + { + var id = tag.Groups[1].Value; //.Remove(tag.Groups[1].Value.Length - 1, 1); + var newLink = urlProvider.GetUrl(int.Parse(id)); + text = text.Replace(tag.Value, "href=\"" + newLink); + } return text; - } + } - // static compiled regex for faster performance - private readonly static Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); + // static compiled regex for faster performance + private readonly static Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", + RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace); - /// - /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. - /// - /// - /// - /// - /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. - /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. - /// - public static string ResolveUrlsFromTextString(string text) - { + /// + /// The RegEx matches any HTML attribute values that start with a tilde (~), those that match are passed to ResolveUrl to replace the tilde with the application path. + /// + /// + /// + /// + /// When used with a Virtual-Directory set-up, this would resolve all URLs correctly. + /// The recommendation is that the "ResolveUrlsFromTextString" option (in umbracoSettings.config) is set to false for non-Virtual-Directory installs. + /// + public static string ResolveUrlsFromTextString(string text) + { if (UmbracoConfig.For.UmbracoSettings().Content.ResolveUrlsFromTextString == false) return text; - using (var timer = DisposableTimer.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) - { - // find all relative urls (ie. urls that contain ~) - var tags = ResolveUrlPattern.Matches(text); - LogHelper.Debug(typeof(IOHelper), "After regex: " + timer.Stopwatch.ElapsedMilliseconds + " matched: " + tags.Count); - foreach (Match tag in tags) - { - var url = ""; - if (tag.Groups[1].Success) - url = tag.Groups[1].Value; + using (var timer = DisposableTimer.DebugDuration(typeof(IOHelper), "ResolveUrlsFromTextString starting", "ResolveUrlsFromTextString complete")) + { + // find all relative urls (ie. urls that contain ~) + var tags = ResolveUrlPattern.Matches(text); + LogHelper.Debug(typeof(IOHelper), "After regex: " + timer.Stopwatch.ElapsedMilliseconds + " matched: " + tags.Count); + foreach (Match tag in tags) + { + var url = ""; + if (tag.Groups[1].Success) + url = tag.Groups[1].Value; - // The richtext editor inserts a slash in front of the url. That's why we need this little fix - // if (url.StartsWith("/")) - // text = text.Replace(url, ResolveUrl(url.Substring(1))); - // else - if (String.IsNullOrEmpty(url) == false) - { - var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); - text = text.Replace(url, resolvedUrl); - } - } - } + // The richtext editor inserts a slash in front of the url. That's why we need this little fix + // if (url.StartsWith("/")) + // text = text.Replace(url, ResolveUrl(url.Substring(1))); + // else + if (String.IsNullOrEmpty(url) == false) + { + var resolvedUrl = (url.Substring(0, 1) == "/") ? IOHelper.ResolveUrl(url.Substring(1)) : IOHelper.ResolveUrl(url); + text = text.Replace(url, resolvedUrl); + } + } + } - return text; - } + return text; + } - } + public static string CleanForXss(string text, params char[] ignoreFromClean) + { + return text.CleanForXss(ignoreFromClean); + } + } } diff --git a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs index 41f4ae232a3c..1d142158e317 100644 --- a/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs +++ b/src/Umbraco.Web/Trees/LegacyTreeDataConverter.cs @@ -254,7 +254,7 @@ internal static Attempt GetUrlAndTitleFromLegacyAction(IAction return Attempt.Succeed( new LegacyUrlAction( "dialogs/republish.aspx?rnd=" + DateTime.UtcNow.Ticks, - "Republishing entire site")); + ui.GetText("actions", "republish"))); case "UmbClientMgr.appActions().actionAssignDomain()": return Attempt.Succeed( new LegacyUrlAction( @@ -415,4 +415,4 @@ public LegacyUrlAction(string url, string dialogTitle, ActionUrlMethod actionMet } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/UI/Controls/UmbracoControl.cs b/src/Umbraco.Web/UI/Controls/UmbracoControl.cs index 4d8bd4ce69a2..bc633b22a30a 100644 --- a/src/Umbraco.Web/UI/Controls/UmbracoControl.cs +++ b/src/Umbraco.Web/UI/Controls/UmbracoControl.cs @@ -84,9 +84,10 @@ public UrlHelper Url get { return _url ?? (_url = new UrlHelper(new RequestContext(new HttpContextWrapper(Context), new RouteData()))); } } - /// - /// Returns the legacy SqlHelper - /// + /// + /// Unused, please do not use + /// + [Obsolete("Obsolete, For querying the database use the new UmbracoDatabase object ApplicationContext.Current.DatabaseContext.Database", false)] protected ISqlHelper SqlHelper { get { return Application.SqlHelper; } diff --git a/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs b/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs index b60c519dd464..2a18413acec1 100644 --- a/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs +++ b/src/Umbraco.Web/UI/Controls/UmbracoUserControl.cs @@ -123,8 +123,9 @@ public UrlHelper Url } /// - /// Returns the legacy SqlHelper + /// Unused, please do not use /// + [Obsolete("Obsolete, For querying the database use the new UmbracoDatabase object ApplicationContext.Current.DatabaseContext.Database", false)] protected ISqlHelper SqlHelper { get { return global::umbraco.BusinessLogic.Application.SqlHelper; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b3c6c0c3597c..02f973a13e67 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -97,18 +97,16 @@ {07fbc26b-2927-4a22-8d96-d644c667fecc} UmbracoExamine - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.dll - False + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.dll True - - ..\packages\AutoMapper.3.0.0\lib\net40\AutoMapper.Net4.dll - False + + ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll True - - ..\packages\ClientDependency.1.9.1\lib\net45\ClientDependency.Core.dll + + ..\packages\ClientDependency.1.9.2\lib\net45\ClientDependency.Core.dll True @@ -118,8 +116,8 @@ ..\packages\dotless.1.4.1.0\lib\dotless.Core.dll - - ..\packages\Examine.0.1.69.0\lib\Examine.dll + + ..\packages\Examine.0.1.70.0\lib\Examine.dll True @@ -312,6 +310,7 @@ + @@ -384,11 +383,12 @@ - + + @@ -916,7 +916,7 @@ Resources.resx - + @@ -2250,6 +2250,7 @@ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v11.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v12.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v15.0 @@ -2265,4 +2266,5 @@ + \ No newline at end of file diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index 8d56a8108469..d878ac3d1d26 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -324,6 +324,8 @@ internal set /// /// Gets the current ApplicationContext /// + [Obsolete("Do not access the ApplicationContext via the UmbracoContext, either inject the ApplicationContext into the services you need or access it via it's own Singleton accessor ApplicationContext.Current")] + [EditorBrowsable(EditorBrowsableState.Never)] public ApplicationContext Application { get; private set; } /// diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index a80188f449bd..f2503b88c686 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -524,9 +524,9 @@ public void Init(HttpApplication app) "Total milliseconds for umbraco request to process: {0}", () => DateTime.Now.Subtract(UmbracoContext.Current.ObjectCreated).TotalMilliseconds); } - OnEndRequest(new EventArgs()); + OnEndRequest(new UmbracoRequestEventArgs(UmbracoContext.Current, new HttpContextWrapper(httpContext))); - DisposeHttpContextItems(httpContext); + DisposeHttpContextItems(httpContext); }; } @@ -536,18 +536,19 @@ public void Dispose() } - #endregion + #endregion #region Events - internal static event EventHandler RouteAttempt; + + public static event EventHandler RouteAttempt; private void OnRouteAttempt(RoutableAttemptEventArgs args) { if (RouteAttempt != null) RouteAttempt(this, args); } - internal static event EventHandler EndRequest; - private void OnEndRequest(EventArgs args) + public static event EventHandler EndRequest; + private void OnEndRequest(UmbracoRequestEventArgs args) { if (EndRequest != null) EndRequest(this, args); diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index d485791d8ca4..f8d737194ed7 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -1,9 +1,9 @@  - - + + - + diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 777fe80782a2..00d1984d1258 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -50,7 +50,7 @@ private content() var profingLogger = new ProfilingLogger( logger, ProfilerResolver.HasCurrent ? ProfilerResolver.Current.Profiler : new LogProfiler(logger)); - + // prepare the persister task // there's always be one task keeping a ref to the runner // so it's safe to just create it as a local var here @@ -71,7 +71,7 @@ private content() // once released, the cache still works but does not write to file anymore, // which is OK with database server messenger but will cause data loss with // another messenger... - + runner.Shutdown(false, true); // wait until flushed _released = true; }); @@ -145,13 +145,17 @@ public string UmbracoXmlDiskCacheFileName } //NOTE: We CANNOT use this for a double check lock because it is a property, not a field and to do double - // check locking in c# you MUST have a volatile field. Even thoug this wraps a volatile field it will still + // check locking in c# you MUST have a volatile field. Even thoug this wraps a volatile field it will still // not work as expected for a double check lock because properties are treated differently in the clr. public virtual bool isInitializing { get { return _xmlContent == null; } } + /// + /// Unused, please do not use + /// + [Obsolete("Obsolete, For querying the database use the new UmbracoDatabase object ApplicationContext.Current.DatabaseContext.Database", false)] protected static ISqlHelper SqlHelper { get { return Application.SqlHelper; } @@ -323,11 +327,17 @@ internal virtual void UpdateSortOrder(IContent c) // this updates the published cache to take care of the situation // without ContentService having to ... what exactly? - // no need to do it if the content is published without unpublished changes, - // though, because in that case the XML will get re-generated with the - // correct sort order. - if (c.Published) - return; + // no need to do it if + // - the content is published without unpublished changes (XML will be re-gen anyways) + // - the content has no published version (not in XML) + // - the sort order has not changed + // note that + // - if it is a new entity is has not published version + // - if Published is dirty and false it's getting unpublished and has no published version + // + if (c.Published) return; + if (c.HasPublishedVersion == false) return; + if (c.WasPropertyDirty("SortOrder") == false) return; using (var safeXml = GetSafeXmlWriter(false)) { @@ -415,7 +425,7 @@ internal void ClearDocumentCache(Document doc) { XmlNode x; - // remove from xml db cache + // remove from xml db cache doc.XmlRemoveFromDB(); // clear xml cache @@ -431,7 +441,7 @@ internal void ClearDocumentCache(Document doc) { var prov = (UmbracoSiteMapProvider)SiteMap.Provider; prov.RemoveNode(doc.Id); - } + } } } @@ -482,7 +492,7 @@ private void ClearContextCache() if (UmbracoContext.Current != null && UmbracoContext.Current.HttpContext != null && UmbracoContext.Current.HttpContext.Items.Contains(XmlContextContentItemKey)) UmbracoContext.Current.HttpContext.Items.Remove(XmlContextContentItemKey); } - + /// /// Load content from database /// @@ -513,7 +523,7 @@ private XmlDocument LoadContentFromDatabase() public void PersistXmlToFile() { } - + internal DateTime GetCacheFileUpdateTime() { //TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed? @@ -562,7 +572,7 @@ private static bool XmlIsImmutable private static bool UseLegacySchema { get { return UmbracoConfig.For.UmbracoSettings().Content.UseLegacyXmlSchema; } - } + } #endregion @@ -766,9 +776,9 @@ public void Dispose() _releaser.Dispose(); _releaser = null; } - + } - + private static string ChildNodesXPath { get @@ -862,9 +872,9 @@ private void SaveXmlToStream(XmlDocument xml, Stream writeStream) // and in addition, writing async is never fully async because // althouth the writer is async, xml.WriteTo() will not async - // that one almost works but... "The elements are indented as long as the element + // that one almost works but... "The elements are indented as long as the element // does not contain mixed content. Once the WriteString or WriteWhitespace method - // is called to write out a mixed element content, the XmlWriter stops indenting. + // is called to write out a mixed element content, the XmlWriter stops indenting. // The indenting resumes once the mixed content element is closed." - says MSDN // about XmlWriterSettings.Indent @@ -1069,7 +1079,7 @@ private static void TransferValuesFromDocumentXmlToPublishedXml(XmlNode document //TODO: This could be faster, might as well just iterate all children and filter // instead of selecting matching children (i.e. iterating all) and then iterating the // filtered items to remove, this also allocates more memory to store the list of children. - // Below we also then do another filtering of child nodes, if we just iterate all children we + // Below we also then do another filtering of child nodes, if we just iterate all children we // can perform both functions more efficiently var dataNodes = publishedNode.SelectNodes(DataNodesXPath); if (dataNodes == null) throw new Exception("oops"); diff --git a/src/Umbraco.Web/umbraco.presentation/helper.cs b/src/Umbraco.Web/umbraco.presentation/helper.cs index 0d133fee662e..2bee9d1e3a91 100644 --- a/src/Umbraco.Web/umbraco.presentation/helper.cs +++ b/src/Umbraco.Web/umbraco.presentation/helper.cs @@ -95,12 +95,14 @@ public static string parseAttribute(IDictionary pageElements, string attributeVa { var attributeValueSplit = (attributeValue).Split(','); + attributeValueSplit = attributeValueSplit.Select(x => x.Trim()).ToArray(); + // before proceeding, we don't want to process anything here unless each item starts/ends with a [ ] // this is because the attribute value could actually just be a json array like [1,2,3] which we don't want to parse // // however, the last one can be a literal, must take care of this! // so here, don't check the last one, which can be just anything - if (attributeValueSplit.Take(attributeValueSplit.Length - 1).All(x => + if (attributeValueSplit.Take(attributeValueSplit.Length - 1).All(x => //must end with [ x.EndsWith("]") && //must start with [ and a special char diff --git a/src/Umbraco.Web/umbraco.presentation/library.cs b/src/Umbraco.Web/umbraco.presentation/library.cs index 5047aef2712e..8bd94aa5b68b 100644 --- a/src/Umbraco.Web/umbraco.presentation/library.cs +++ b/src/Umbraco.Web/umbraco.presentation/library.cs @@ -77,9 +77,13 @@ private static UmbracoHelper GetUmbracoHelper() #region Properties + /// + /// Unused, please do not use + /// + [Obsolete("Obsolete, For querying the database use the new UmbracoDatabase object ApplicationContext.Current.DatabaseContext.Database", false)] protected static ISqlHelper SqlHelper { - get { return umbraco.BusinessLogic.Application.SqlHelper; } + get { return Application.SqlHelper; } } #endregion @@ -1275,8 +1279,9 @@ public static XPathNodeIterator GetPreValues(int DataTypeId) XmlDocument xd = new XmlDocument(); xd.LoadXml(""); - using (IRecordsReader dr = SqlHelper.ExecuteReader("Select id, [value] from cmsDataTypeprevalues where DataTypeNodeId = @dataTypeId order by sortorder", - SqlHelper.CreateParameter("@dataTypeId", DataTypeId))) + using (var sqlHelper = Application.SqlHelper) + using (IRecordsReader dr = sqlHelper.ExecuteReader("Select id, [value] from cmsDataTypeprevalues where DataTypeNodeId = @dataTypeId order by sortorder", + sqlHelper.CreateParameter("@dataTypeId", DataTypeId))) { while (dr.Read()) { @@ -1298,8 +1303,9 @@ public static string GetPreValueAsString(int Id) { try { - return SqlHelper.ExecuteScalar("select [value] from cmsDataTypePreValues where id = @id", - SqlHelper.CreateParameter("@id", Id)); + using (var sqlHelper = Application.SqlHelper) + return sqlHelper.ExecuteScalar("select [value] from cmsDataTypePreValues where id = @id", + sqlHelper.CreateParameter("@id", Id)); } catch { diff --git a/src/Umbraco.Web/umbraco.presentation/macro.cs b/src/Umbraco.Web/umbraco.presentation/macro.cs index 77027749e2e2..6d2936cd9a86 100644 --- a/src/Umbraco.Web/umbraco.presentation/macro.cs +++ b/src/Umbraco.Web/umbraco.presentation/macro.cs @@ -64,6 +64,10 @@ public class macro private const string MacrosAddedKey = "macrosAdded"; public IList Exceptions = new List(); + /// + /// Unused, please do not use + /// + [Obsolete("Obsolete, For querying the database use the new UmbracoDatabase object ApplicationContext.Current.DatabaseContext.Database", false)] protected static ISqlHelper SqlHelper { get { return Application.SqlHelper; } diff --git a/src/Umbraco.Web/umbraco.presentation/template.cs b/src/Umbraco.Web/umbraco.presentation/template.cs index dfdad324e287..4290ce2acfde 100644 --- a/src/Umbraco.Web/umbraco.presentation/template.cs +++ b/src/Umbraco.Web/umbraco.presentation/template.cs @@ -468,12 +468,14 @@ private String FindAttribute(Hashtable attributes, String key) #endregion - + /// + /// Unused, please do not use + /// + [Obsolete("Obsolete, For querying the database use the new UmbracoDatabase object ApplicationContext.Current.DatabaseContext.Database", false)] protected static ISqlHelper SqlHelper { get { return Application.SqlHelper; } } - #region constructors public static string GetMasterPageName(int templateID) @@ -497,11 +499,12 @@ public template(int templateID) var t = ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem