diff --git a/PlexRequests.Core.Migration/IMigration.cs b/PlexRequests.Core.Migration/IMigration.cs new file mode 100644 index 000000000..572ebc930 --- /dev/null +++ b/PlexRequests.Core.Migration/IMigration.cs @@ -0,0 +1,37 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: IMigration.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Data; + +namespace PlexRequests.Core.Migration +{ + public interface IMigration + { + int Version { get; } + void Start(IDbConnection con); + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/IMigrationRunner.cs b/PlexRequests.Core.Migration/IMigrationRunner.cs new file mode 100644 index 000000000..4ccd615e8 --- /dev/null +++ b/PlexRequests.Core.Migration/IMigrationRunner.cs @@ -0,0 +1,7 @@ +namespace PlexRequests.Core.Migration +{ + public interface IMigrationRunner + { + void MigrateToLatest(); + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/Migrate.cs b/PlexRequests.Core.Migration/Migrate.cs new file mode 100644 index 000000000..e5266aa5b --- /dev/null +++ b/PlexRequests.Core.Migration/Migrate.cs @@ -0,0 +1,33 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Migrate.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion +namespace PlexRequests.Core.Migration +{ + public class Migrate + { + + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/MigrationAttribute.cs b/PlexRequests.Core.Migration/MigrationAttribute.cs new file mode 100644 index 000000000..b117c1127 --- /dev/null +++ b/PlexRequests.Core.Migration/MigrationAttribute.cs @@ -0,0 +1,43 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Migration.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System; + +namespace PlexRequests.Core.Migration +{ + [AttributeUsage(AttributeTargets.Class)] + public class Migration : Attribute + { + public Migration(int version, string description) + { + Version = version; + Description = description; + } + public int Version { get; private set; } + public string Description { get; private set; } + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/MigrationRunner.cs b/PlexRequests.Core.Migration/MigrationRunner.cs new file mode 100644 index 000000000..c8935031a --- /dev/null +++ b/PlexRequests.Core.Migration/MigrationRunner.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.Linq; +using System.Reflection; +using Ninject; +using PlexRequests.Store; + +namespace PlexRequests.Core.Migration +{ + public class MigrationRunner : IMigrationRunner + { + public MigrationRunner(ISqliteConfiguration db, IKernel kernel) + { + Db = db; + Kernel = kernel; + } + + private IKernel Kernel { get; } + private ISqliteConfiguration Db { get; } + + public void MigrateToLatest() + { + var con = Db.DbConnection(); + var versions = GetMigrations().OrderBy(x => x.Key); + + var dbVersion = con.GetVersionInfo().OrderByDescending(x => x.Version).FirstOrDefault(); + if (dbVersion == null) + { + dbVersion = new TableCreation.VersionInfo { Version = 0 }; + } + foreach (var v in versions) + { + if (v.Value.Version > dbVersion.Version) + { + // Assuming only one constructor + var ctor = v.Key.GetConstructors().FirstOrDefault(); + var dependencies = new List(); + + foreach (var param in ctor.GetParameters()) + { + Console.WriteLine(string.Format( + "Param {0} is named {1} and is of type {2}", + param.Position, param.Name, param.ParameterType)); + + var dep = Kernel.Get(param.ParameterType); + dependencies.Add(dep); + } + + var method = v.Key.GetMethod("Start"); + if (method != null) + { + object result = null; + var classInstance = Activator.CreateInstance(v.Key, dependencies.Any() ? dependencies.ToArray() : null); + + var parametersArray = new object[] { Db.DbConnection() }; + + method.Invoke(classInstance, parametersArray); + } + } + } + } + + public static Dictionary GetMigrations() + { + var migrationTypes = GetTypesWithHelpAttribute(Assembly.GetAssembly(typeof(MigrationRunner))); + + var version = new Dictionary(); + + foreach (var t in migrationTypes) + { + var customAttributes = (Migration[])t.GetCustomAttributes(typeof(Migration), true); + if (customAttributes.Length > 0) + { + var attr = customAttributes[0]; + + version.Add(t, new MigrationModel { Version = attr.Version, Description = attr.Description }); + } + } + return version; + } + + private static IEnumerable GetTypesWithHelpAttribute(Assembly assembly) + { + return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof(Migration), true).Length > 0); + } + + public class MigrationModel + { + public int Version { get; set; } + public string Description { get; set; } + } + } +} diff --git a/PlexRequests.Core.Migration/Migrations/BaseMigration.cs b/PlexRequests.Core.Migration/Migrations/BaseMigration.cs new file mode 100644 index 000000000..e6bcff34b --- /dev/null +++ b/PlexRequests.Core.Migration/Migrations/BaseMigration.cs @@ -0,0 +1,53 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: BaseMigration.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Collections.Generic; +using System.Data; +using System.Linq; +using PlexRequests.Store; + +namespace PlexRequests.Core.Migration.Migrations +{ + public abstract class BaseMigration + { + protected void UpdateSchema(IDbConnection con, int version) + { + var migrations = MigrationRunner.GetMigrations(); + var model = migrations.Select(x => x.Value).FirstOrDefault(x => x.Version == version); + + if (model != null) + { + con.AddVersionInfo(new TableCreation.VersionInfo { Version = model.Version, Description = model.Description }); + } + } + + protected IEnumerable GetVersionInfo(IDbConnection con) + { + return con.GetVersionInfo(); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/Migrations/Version195.cs b/PlexRequests.Core.Migration/Migrations/Version195.cs new file mode 100644 index 000000000..01724f3a0 --- /dev/null +++ b/PlexRequests.Core.Migration/Migrations/Version195.cs @@ -0,0 +1,63 @@ +#region Copyright +// /************************************************************************ +// Copyright (c) 2016 Jamie Rees +// File: Version195.cs +// Created By: Jamie Rees +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// ************************************************************************/ +#endregion + +using System.Data; +using PlexRequests.Core.SettingModels; + +namespace PlexRequests.Core.Migration.Migrations +{ + [Migration(1950, "v1.9.5.0")] + public class Version195 : BaseMigration, IMigration + { + public Version195(ISettingsService plexRequestSettings, ISettingsService news) + { + PlexRequestSettings = plexRequestSettings; + NewsletterSettings = news; + } + public int Version => 1950; + + private ISettingsService PlexRequestSettings { get; } + private ISettingsService NewsletterSettings { get; } + + public void Start(IDbConnection con) + { + var plex = PlexRequestSettings.GetSettings(); + + var newsLetter = NewsletterSettings.GetSettings(); + if (plex.SendRecentlyAddedEmail) + { + newsLetter.SendRecentlyAddedEmail = plex.SendRecentlyAddedEmail; + plex.SendRecentlyAddedEmail = false; + + PlexRequestSettings.SaveSettings(plex); + NewsletterSettings.SaveSettings(newsLetter); + } + + UpdateSchema(con, Version); + } + } +} \ No newline at end of file diff --git a/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj new file mode 100644 index 000000000..bb82fe012 --- /dev/null +++ b/PlexRequests.Core.Migration/PlexRequests.Core.Migration.csproj @@ -0,0 +1,77 @@ + + + + + Debug + AnyCPU + {8406EE57-D533-47C0-9302-C6B5F8C31E55} + Library + Properties + PlexRequests.Core.Migration + PlexRequests.Core.Migration + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\Assemblies\Mono.Data.Sqlite.dll + + + ..\packages\Ninject.3.2.0.0\lib\net45-full\Ninject.dll + + + + + + + + + + + + + + + + + + + + + + + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} + PlexRequests.Core + + + {92433867-2B7B-477B-A566-96C382427525} + PlexRequests.Store + + + + + \ No newline at end of file diff --git a/PlexRequests.Core.Migration/Properties/AssemblyInfo.cs b/PlexRequests.Core.Migration/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..6e44d89b7 --- /dev/null +++ b/PlexRequests.Core.Migration/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("PlexRequests.Core.Migration")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("PlexRequests.Core.Migration")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("8406ee57-d533-47c0-9302-c6b5f8c31e55")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/PlexRequests.Core/Setup.cs b/PlexRequests.Core/Setup.cs index e16613161..c5d5ddc58 100644 --- a/PlexRequests.Core/Setup.cs +++ b/PlexRequests.Core/Setup.cs @@ -60,6 +60,8 @@ namespace PlexRequests.Core TableCreation.Vacuum(Db.DbConnection()); } + + // The below code is obsolete, we should use PlexRequests.Core.Migrations.MigrationRunner var version = CheckSchema(); if (version > 0) { @@ -72,10 +74,6 @@ namespace PlexRequests.Core { MigrateToVersion1910(); } - if (version > 1943 && version <= 1945) - { - MigrateToVersion1945(); - } } return Db.DbConnection().ConnectionString; @@ -279,31 +277,5 @@ namespace PlexRequests.Core Log.Error(e); } } - - - /// - /// Migrates to version1945 - /// - public void MigrateToVersion1945() - { - try - { - var settings = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var plex = settings.GetSettings(); - var newsLetterSettings = new SettingsServiceV2(new SettingsJsonRepository(Db, new MemoryCacheProvider())); - var newsLetter = newsLetterSettings.GetSettings(); - if (plex.SendRecentlyAddedEmail) - { - newsLetter.SendRecentlyAddedEmail = plex.SendRecentlyAddedEmail; - plex.SendRecentlyAddedEmail = false; - settings.SaveSettings(plex); - newsLetterSettings.SaveSettings(newsLetter); - } - } - catch (Exception e) - { - Log.Error(e); - } - } } } diff --git a/PlexRequests.Store/SqlTables.sql b/PlexRequests.Store/SqlTables.sql index 74f31d0be..4ac055c24 100644 --- a/PlexRequests.Store/SqlTables.sql +++ b/PlexRequests.Store/SqlTables.sql @@ -59,6 +59,12 @@ CREATE TABLE IF NOT EXISTS DBInfo SchemaVersion INTEGER ); +CREATE TABLE IF NOT EXISTS VersionInfo +( + Version INTEGER NOT NULL, + Description VARCHAR(100) NOT NULL +); + CREATE TABLE IF NOT EXISTS ScheduledJobs ( Id INTEGER PRIMARY KEY AUTOINCREMENT, diff --git a/PlexRequests.Store/TableCreation.cs b/PlexRequests.Store/TableCreation.cs index 3e6336deb..7aca3a79a 100644 --- a/PlexRequests.Store/TableCreation.cs +++ b/PlexRequests.Store/TableCreation.cs @@ -24,6 +24,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // *********************************************************************** #endregion + +using System.Collections.Generic; using System.Data; using System.Linq; using Dapper; @@ -109,7 +111,29 @@ namespace PlexRequests.Store con.Close(); } + public static IEnumerable GetVersionInfo(this IDbConnection con) + { + con.Open(); + var result = con.Query("SELECT * FROM VersionInfo"); + con.Close(); + + return result; + } + + public static void AddVersionInfo(this IDbConnection con, VersionInfo ver) + { + con.Open(); + con.Insert(ver); + con.Close(); + } + + [Table("VersionInfo")] + public class VersionInfo + { + public int Version { get; set; } + public string Description { get; set; } + } [Table("DBInfo")] public class DbInfo diff --git a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs index 1382fd9c1..57cb76235 100644 --- a/PlexRequests.UI/NinjectModules/ConfigurationModule.cs +++ b/PlexRequests.UI/NinjectModules/ConfigurationModule.cs @@ -32,6 +32,7 @@ using Nancy.Authentication.Forms; using Ninject.Modules; using PlexRequests.Core; +using PlexRequests.Core.Migration; using PlexRequests.Helpers; using PlexRequests.Services.Interfaces; using PlexRequests.Services.Notification; @@ -47,6 +48,7 @@ namespace PlexRequests.UI.NinjectModules Bind().To().WithConstructorArgument("provider", new SqliteFactory()); Bind().To().WithConstructorArgument("provider", new SqliteFactory()); Bind().To(); + Bind().To(); Bind().To(); diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj index 6c377cad6..d51e3a499 100644 --- a/PlexRequests.UI/PlexRequests.UI.csproj +++ b/PlexRequests.UI/PlexRequests.UI.csproj @@ -754,6 +754,10 @@ {8CB8D235-2674-442D-9C6A-35FCAEEB160D} PlexRequests.Api + + {8406EE57-D533-47C0-9302-C6B5F8C31E55} + PlexRequests.Core.Migration + {DD7DC444-D3BF-4027-8AB9-EFC71F5EC581} PlexRequests.Core diff --git a/PlexRequests.UI/Startup.cs b/PlexRequests.UI/Startup.cs index c61f8766b..3faa00ab4 100644 --- a/PlexRequests.UI/Startup.cs +++ b/PlexRequests.UI/Startup.cs @@ -32,6 +32,7 @@ using Ninject.Planning.Bindings.Resolvers; using NLog; using Owin; +using PlexRequests.Core.Migration; using PlexRequests.Services.Jobs; using PlexRequests.UI.Helpers; using PlexRequests.UI.Jobs; @@ -67,6 +68,10 @@ namespace PlexRequests.UI Debug.WriteLine("Finished bootstrapper"); var scheduler = new Scheduler(); scheduler.StartScheduler(); + + var runner = kernel.Get(); + runner.MigrateToLatest(); + } catch (Exception exception) { diff --git a/PlexRequests.sln b/PlexRequests.sln index ec6287fad..91f74cf72 100644 --- a/PlexRequests.sln +++ b/PlexRequests.sln @@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Automation.Pag EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequestes.Automation.Helpers", "PlexRequestes.Automation.Helpers\PlexRequestes.Automation.Helpers.csproj", "{DC8BACEF-C284-4A8F-A9AA-7F49EFABA288}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlexRequests.Core.Migration", "PlexRequests.Core.Migration\PlexRequests.Core.Migration.csproj", "{8406EE57-D533-47C0-9302-C6B5F8C31E55}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -108,6 +110,10 @@ Global {DC8BACEF-C284-4A8F-A9AA-7F49EFABA288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DC8BACEF-C284-4A8F-A9AA-7F49EFABA288}.Debug|Any CPU.Build.0 = Debug|Any CPU {DC8BACEF-C284-4A8F-A9AA-7F49EFABA288}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8406EE57-D533-47C0-9302-C6B5F8C31E55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8406EE57-D533-47C0-9302-C6B5F8C31E55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8406EE57-D533-47C0-9302-C6B5F8C31E55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8406EE57-D533-47C0-9302-C6B5F8C31E55}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE