Migrating to Ratermania.Automation

This commit is contained in:
2021-01-25 09:47:50 -05:00
parent 378c072360
commit b0935e9214
10 changed files with 154 additions and 193 deletions

View File

@@ -2,3 +2,9 @@
# CA2007: Consider calling ConfigureAwait on the awaited task # CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = silent dotnet_diagnostic.CA2007.severity = silent
# Default severity for analyzer diagnostics with category 'Design'
dotnet_analyzer_diagnostic.category-Design.severity = silent
# Default severity for analyzer diagnostics with category 'Globalization'
dotnet_analyzer_diagnostic.category-Globalization.severity = silent

View File

@@ -32,14 +32,14 @@ namespace PartSource.Automation.Factories
case nameof(StatusCheck): case nameof(StatusCheck):
return _serviceProvider.GetService<StatusCheck>(); return _serviceProvider.GetService<StatusCheck>();
case nameof(TestJob): //case nameof(TestJob):
return new TestJob(); // return new TestJob();
case nameof(UpdateFitment): case nameof(UpdateFitment):
return _serviceProvider.GetService<UpdateFitment>(); return _serviceProvider.GetService<UpdateFitment>();
case nameof(UpdatePricing): //case nameof(UpdatePricing):
return _serviceProvider.GetService<UpdatePricing>(); // return _serviceProvider.GetService<UpdatePricing>();
case nameof(UpdatePositioning): case nameof(UpdatePositioning):
return _serviceProvider.GetService<UpdatePositioning>(); return _serviceProvider.GetService<UpdatePositioning>();

View File

@@ -1,21 +1,32 @@
using PartSource.Automation.Jobs.Interfaces; using Ratermania.Automation.Interfaces;
using PartSource.Automation.Models; using PartSource.Automation.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace PartSource.Automation.Jobs namespace PartSource.Automation.Jobs
{ {
public class TestJob : IAutomationJob public class TestJob : IAutomationJob
{ {
public async Task<AutomationJobResult> Run() private readonly ILogger<TestJob> _logger;
public TestJob(ILogger<TestJob> logger)
{ {
return new AutomationJobResult _logger = logger;
}
#pragma warning disable CS1998, CA1303
public async Task Run()
{ {
Message = "Test job ran successfully from the new server", await Task.Delay(5000);
IsSuccess = true
}; _logger.LogInformation("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc scelerisque congue euismod. Curabitur enim eros, sollicitudin ac purus eget, dignissim mattis augue. In quam sapien, tincidunt et elementum vitae, interdum vitae sem.");
} _logger.LogWarning("Praesent feugiat sapien non suscipit faucibus. Mauris fermentum ut augue a feugiat. Integer felis sem, laoreet et augue at, finibus maximus ex. Fusce sit amet erat non tortor porta condimentum condimentum quis ipsum.");
_logger.LogError("Sed fringilla placerat turpis, sed tristique mi malesuada quis. Sed a justo erat. In iaculis, orci pulvinar tempor accumsan, mi leo rutrum lorem, ut egestas arcu ligula sodales dolor.");
_logger.LogCritical("Donec pulvinar vehicula massa. Praesent non erat tortor. Duis posuere tortor sed odio iaculis, sit amet eleifend est tincidunt. Suspendisse rhoncus eros id purus aliquet, ut porttitor lectus eleifend.");
}
#pragma warning restore CS1998, CA1303
} }
} }

View File

@@ -1,6 +1,6 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using PartSource.Automation.Jobs.Interfaces; using Ratermania.Automation.Interfaces;
using PartSource.Automation.Models; using PartSource.Automation.Models;
using PartSource.Data; using PartSource.Data;
using PartSource.Data.Models; using PartSource.Data.Models;
@@ -11,22 +11,24 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace PartSource.Automation.Jobs namespace PartSource.Automation.Jobs
{ {
public class UpdatePricing : IAutomationJob public class UpdatePricing : IAutomationJob
{ {
private readonly ILogger<UpdatePricing> _logger;
private readonly PartSourceContext _partSourceContext; private readonly PartSourceContext _partSourceContext;
private readonly ShopifyClient _shopifyClient; private readonly ShopifyClient _shopifyClient;
public UpdatePricing(PartSourceContext partSourceContext, ShopifyClient shopifyClient) public UpdatePricing(ILogger<UpdatePricing> logger, PartSourceContext partSourceContext, ShopifyClient shopifyClient)
{ {
_logger = logger;
_partSourceContext = partSourceContext; _partSourceContext = partSourceContext;
_shopifyClient = shopifyClient; _shopifyClient = shopifyClient;
} }
public async Task<AutomationJobResult> Run() public async Task Run()
{ {
IEnumerable<Product> products = null; IEnumerable<Product> products = null;
IEnumerable<PartPrice> prices = null; IEnumerable<PartPrice> prices = null;
@@ -40,12 +42,8 @@ namespace PartSource.Automation.Jobs
catch (Exception ex) catch (Exception ex)
{ {
// TODO: Logging _logger.LogError(ex, "Failed to get the initial set of products from Shopify.");
return new AutomationJobResult return;
{
Message = "Failed to get products from Shopify",
IsSuccess = false
};
} }
while (products != null && products.Any()) while (products != null && products.Any())
@@ -54,7 +52,6 @@ namespace PartSource.Automation.Jobs
{ {
if (product.Variants.Length > 0) if (product.Variants.Length > 0)
{ {
Variant variant = product.Variants[0]; Variant variant = product.Variants[0];
PartPrice partPrice = prices.Where(p => p.SKU == variant.Sku).FirstOrDefault(); PartPrice partPrice = prices.Where(p => p.SKU == variant.Sku).FirstOrDefault();
@@ -63,8 +60,7 @@ namespace PartSource.Automation.Jobs
continue; continue;
} }
try
{
if (product.Variants[0].Price != partPrice.Your_Price.Value || product.Variants[0].CompareAtPrice != partPrice.Compare_Price.Value) if (product.Variants[0].Price != partPrice.Your_Price.Value || product.Variants[0].CompareAtPrice != partPrice.Compare_Price.Value)
{ {
product.Variants[0].Price = partPrice.Your_Price.Value; product.Variants[0].Price = partPrice.Your_Price.Value;
@@ -89,48 +85,29 @@ namespace PartSource.Automation.Jobs
await _shopifyClient.Products.Update(product); await _shopifyClient.Products.Update(product);
updateCount++; updateCount++;
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine("bad update"); _logger.LogWarning(ex, $"Failed to update pricing for SKU {variant.Sku}");
} }
} }
} }
catch (Exception ex)
{
Console.WriteLine("failed getting parts");
} }
}
}
// _partSourceContext.SaveChanges();
try try
{ {
//await _shopifyClient.Products.SaveChanges();
products = await _shopifyClient.Products.GetNext(); products = await _shopifyClient.Products.GetNext();
Console.SetCursorPosition(0, 2); _logger.LogInformation($"Total updated: {updateCount}");
Console.Clear();
Console.Write($"Updated: {updateCount} ");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogWarning(ex, "Failed to get the next set of products. Retrying");
products = await _shopifyClient.Products.GetPrevious(); products = await _shopifyClient.Products.GetPrevious();
} }
} }
return new AutomationJobResult
{
Message = $"The nightly pricing update has completed. {updateCount} products were updated.",
IsSuccess = true
};
} }
} }
} }

View File

@@ -13,13 +13,16 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.1.11" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.11" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="3.1.11" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="3.1.11" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="3.1.11" />
<PackageReference Include="Ratermania.Shopify" Version="1.3.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\..\shopify\Shopify\Shopify.csproj" /> <ProjectReference Include="..\..\shopify\Automation\Automation.csproj" />
<ProjectReference Include="..\PartSource.Data\PartSource.Data.csproj" /> <ProjectReference Include="..\PartSource.Data\PartSource.Data.csproj" />
<ProjectReference Include="..\PartSource.Services\PartSource.Services.csproj" /> <ProjectReference Include="..\PartSource.Services\PartSource.Services.csproj" />
</ItemGroup> </ItemGroup>

View File

@@ -2,6 +2,8 @@
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using PartSource.Automation.Factories; using PartSource.Automation.Factories;
using PartSource.Automation.Jobs; using PartSource.Automation.Jobs;
using PartSource.Automation.Jobs.Interfaces; using PartSource.Automation.Jobs.Interfaces;
@@ -11,107 +13,71 @@ using PartSource.Automation.Services;
using PartSource.Data; using PartSource.Data;
using PartSource.Data.AutoMapper; using PartSource.Data.AutoMapper;
using PartSource.Services; using PartSource.Services;
using Ratermania.Automation;
using Ratermania.Automation.DependencyInjection;
using Ratermania.Automation.Logging;
using Ratermania.Shopify; using Ratermania.Shopify;
using Ratermania.Shopify.DependencyInjection; using Ratermania.Shopify.DependencyInjection;
using System; using System;
using System.IO; using System.IO;
using System.Threading.Tasks;
namespace PartSource.Automation namespace PartSource.Automation
{ internal class Program {
class Program
{ {
private static void Main(string[] args) static async Task Main(string[] args)
{ {
IServiceProvider serviceProvider = ConfigureServices(); using IHost host = CreateHostBuilder().Build();
JobFactory jobFactory = serviceProvider.GetService<JobFactory>(); await host.StartAsync();
EmailService emailService = serviceProvider.GetService<EmailService>();
foreach (string arg in args)
{
Console.Write($"Running job {arg}... ");
try
{
IAutomationJob job = jobFactory.Build(arg);
AutomationJobResult result = job.Run().Result;
if (result.IsSuccess)
{
emailService.Send($"{arg} Completed Successfully", result.Message);
} }
else private static IHostBuilder CreateHostBuilder()
{ {
emailService.Send($"{arg} Failed", result.Message); return Host.CreateDefaultBuilder()
} .ConfigureAppConfiguration(builder =>
Console.WriteLine("Done");
}
catch (Exception ex)
{ {
Console.WriteLine(ex.Message); string environment = Environment.GetEnvironmentVariable("AUTOMATION_ENVIRONMENT");
}
}
}
private static IServiceProvider ConfigureServices() builder.SetBasePath(Directory.GetCurrentDirectory())
{
string environment = Environment.GetEnvironmentVariable("PS_AUTOMATION_ENVIRONMENT");
IConfigurationBuilder builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
.AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true); .AddJsonFile($"appsettings.{environment}.json", optional: true, reloadOnChange: true);
})
IConfigurationRoot configuration = builder.Build(); .ConfigureServices((builder, services) =>
{
EmailConfiguration emailConfiguration = new EmailConfiguration(); services.AddDbContext<PartSourceContext>(options =>
FtpConfiguration ftpConfiguration = new FtpConfiguration(); options.UseSqlServer(builder.Configuration.GetConnectionString("PartSourceDatabase"), opts => opts.EnableRetryOnFailure())
SsisConfiguration ssisConfiguration = new SsisConfiguration();
configuration.Bind("emailConfiguration", emailConfiguration);
configuration.Bind("ftpConfiguration", ftpConfiguration);
configuration.Bind("ssisConfiguration", ssisConfiguration);
ServiceProvider serviceProvider = new ServiceCollection()
.AddDbContext<PartSourceContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("PartSourceDatabase"), opts => opts.EnableRetryOnFailure())
) )
.AddShopify(options => .AddShopify(options =>
{ {
options.ApiKey = configuration["Shopify:ApiKey"]; options.ApiKey = builder.Configuration["Shopify:ApiKey"];
options.ApiSecret = configuration["Shopify:ApiSecret"]; options.ApiSecret = builder.Configuration["Shopify:ApiSecret"];
options.ApiVersion = "2020-01"; options.ApiVersion = "2020-01";
options.ShopDomain = configuration["Shopify:ShopDomain"]; options.ShopDomain = builder.Configuration["Shopify:ShopDomain"];
}) })
.AddAutoMapper(typeof(PartSourceProfile)) .AddAutomation(options =>
{
options.HasBaseInterval(new TimeSpan(0, 1, 0))
.HasMaxFailures(5)
.HasJob<TestJob>(options =>
{
options.HasInterval(new TimeSpan(0, 5, 0));
})
.AddApiServer();
})
.AddSingleton(emailConfiguration) .AddAutoMapper(typeof(PartSourceProfile));
.AddSingleton(ftpConfiguration) })
.AddSingleton(ssisConfiguration) .ConfigureLogging((builder, logging) =>
.AddSingleton<NexpartService>() {
.AddSingleton<VehicleService>() logging.AddEventLog();
.AddSingleton<SsisService>()
.AddSingleton<FtpService>()
.AddSingleton<EmailService>()
.AddSingleton<AddAndUpdateProducts>()
//.AddSingleton<BuildCategories>()
//.AddSingleton<BackupProducts>()
//.AddSingleton<BuildVehicleCache>()
.AddSingleton<DeleteProducts>()
.AddSingleton<StatusCheck>()
.AddSingleton<UpdateFitment>()
.AddSingleton<UpdatePricing>()
.AddSingleton<UpdatePositioning>()
.AddSingleton<ExecuteSsisPackages>()
.AddSingleton<JobFactory>()
.BuildServiceProvider();
return serviceProvider; logging.AddProvider(new AutomationLoggerProvider());
});
} }
} }
} }

View File

@@ -33,7 +33,6 @@ namespace PartSource.Automation.Services
CreateNoWindow = false, CreateNoWindow = false,
RedirectStandardOutput = true, RedirectStandardOutput = true,
RedirectStandardError = true RedirectStandardError = true
} }
}; };

View File

@@ -28,5 +28,8 @@
"ApiKey": "88f931933b566ade1fc92c6a39f04b34", "ApiKey": "88f931933b566ade1fc92c6a39f04b34",
"ApiSecret": "527a3b4213c2c7ecb214728a899052df", "ApiSecret": "527a3b4213c2c7ecb214728a899052df",
"ShopDomain": "partsource.myshopify.com" "ShopDomain": "partsource.myshopify.com"
},
"LogLevel": {
"Default": "Information"
} }
} }

View File

@@ -23,9 +23,5 @@
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\shopify\Shopify\Shopify.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -11,13 +11,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PartSource.Services", "Part
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PartSource.Automation", "PartSource.Automation\PartSource.Automation.csproj", "{C85D675B-A76C-4F9C-9C57-1E063211C946}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PartSource.Automation", "PartSource.Automation\PartSource.Automation.csproj", "{C85D675B-A76C-4F9C-9C57-1E063211C946}"
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Shopify", "..\shopify\Shopify\Shopify.csproj", "{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1631E7EC-E54D-4F3F-9800-6EE1D5B2CB48}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1631E7EC-E54D-4F3F-9800-6EE1D5B2CB48}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Automation", "..\shopify\Automation\Automation.csproj", "{40E3046C-7B99-4F92-8626-1EF2892DFDCD}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Also Debug|Any CPU = Also Debug|Any CPU Also Debug|Any CPU = Also Debug|Any CPU
@@ -103,24 +103,24 @@ Global
{C85D675B-A76C-4F9C-9C57-1E063211C946}.Release|x64.Build.0 = Release|Any CPU {C85D675B-A76C-4F9C-9C57-1E063211C946}.Release|x64.Build.0 = Release|Any CPU
{C85D675B-A76C-4F9C-9C57-1E063211C946}.Release|x86.ActiveCfg = Release|Any CPU {C85D675B-A76C-4F9C-9C57-1E063211C946}.Release|x86.ActiveCfg = Release|Any CPU
{C85D675B-A76C-4F9C-9C57-1E063211C946}.Release|x86.Build.0 = Release|Any CPU {C85D675B-A76C-4F9C-9C57-1E063211C946}.Release|x86.Build.0 = Release|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Also Debug|Any CPU.ActiveCfg = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Also Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Also Debug|Any CPU.Build.0 = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Also Debug|Any CPU.Build.0 = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Also Debug|x64.ActiveCfg = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Also Debug|x64.ActiveCfg = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Also Debug|x64.Build.0 = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Also Debug|x64.Build.0 = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Also Debug|x86.ActiveCfg = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Also Debug|x86.ActiveCfg = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Also Debug|x86.Build.0 = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Also Debug|x86.Build.0 = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Debug|Any CPU.Build.0 = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Debug|x64.ActiveCfg = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Debug|x64.ActiveCfg = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Debug|x64.Build.0 = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Debug|x64.Build.0 = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Debug|x86.ActiveCfg = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Debug|x86.ActiveCfg = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Debug|x86.Build.0 = Debug|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Debug|x86.Build.0 = Debug|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Release|Any CPU.ActiveCfg = Release|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Release|Any CPU.Build.0 = Release|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Release|Any CPU.Build.0 = Release|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Release|x64.ActiveCfg = Release|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Release|x64.ActiveCfg = Release|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Release|x64.Build.0 = Release|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Release|x64.Build.0 = Release|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Release|x86.ActiveCfg = Release|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Release|x86.ActiveCfg = Release|Any CPU
{FDC22085-4C5A-4CCD-B0DB-9D31F90ECE90}.Release|x86.Build.0 = Release|Any CPU {40E3046C-7B99-4F92-8626-1EF2892DFDCD}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE