diff --git a/Samples.sln b/Samples.sln index 9e8c364861..349b0c8cda 100644 --- a/Samples.sln +++ b/Samples.sln @@ -111,6 +111,12 @@ Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicToCSharpConverte EndProject Project("{778DAE3C-4631-46EA-AA77-85C1314464D9}") = "VisualBasicToCSharpConverter.UnitTests", "samples\VisualBasic\VisualBasicToCSharpConverter\VisualBasicToCSharpConverter.Test\VisualBasicToCSharpConverter.UnitTests.vbproj", "{5B7D7569-B5EE-4C01-9AFA-BC1958588160}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SourceGenerators", "SourceGenerators", "{14D18F51-6B59-49D5-9AB7-08B38417A459}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSamples", "samples\CSharp\SourceGenerators\SourceGeneratorSamples\SourceGeneratorSamples.csproj", "{2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratedDemo", "samples\CSharp\SourceGenerators\GeneratedDemo\GeneratedDemo.csproj", "{EC4DB63B-C2B4-4D06-AF98-15253035C6D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -269,6 +275,14 @@ Global {5B7D7569-B5EE-4C01-9AFA-BC1958588160}.Debug|Any CPU.Build.0 = Debug|Any CPU {5B7D7569-B5EE-4C01-9AFA-BC1958588160}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B7D7569-B5EE-4C01-9AFA-BC1958588160}.Release|Any CPU.Build.0 = Release|Any CPU + {2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045}.Release|Any CPU.Build.0 = Release|Any CPU + {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EC4DB63B-C2B4-4D06-AF98-15253035C6D5}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -325,6 +339,9 @@ Global {8E1C9AEC-6EF1-43A8-A378-52C5C0E40532} = {CDA94F62-E35A-4913-8045-D9D42416513C} {ECB83742-8023-4609-B139-D7B78DD66ED9} = {8E1C9AEC-6EF1-43A8-A378-52C5C0E40532} {5B7D7569-B5EE-4C01-9AFA-BC1958588160} = {8E1C9AEC-6EF1-43A8-A378-52C5C0E40532} + {14D18F51-6B59-49D5-9AB7-08B38417A459} = {C3FB27E9-C8EE-4F76-B0AA-7CD67A7E652B} + {2ADE5CFA-5DF4-44A9-BD67-E884BCFBA045} = {14D18F51-6B59-49D5-9AB7-08B38417A459} + {EC4DB63B-C2B4-4D06-AF98-15253035C6D5} = {14D18F51-6B59-49D5-9AB7-08B38417A459} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B849838B-3D7A-4B6B-BE07-285DCB1588F4} diff --git a/global.json b/global.json index f5c64ea514..264f27e2ea 100644 --- a/global.json +++ b/global.json @@ -1,12 +1,12 @@ { "tools": { - "dotnet": "3.0.101", + "dotnet": "3.1.300-preview-015115", "vs": { - "version": "16.3" + "version": "16.6" }, "xcopy-msbuild": "16.3.0-alpha" }, "msbuild-sdks": { - "Microsoft.DotNet.Arcade.Sdk": "5.0.0-beta.20117.3" + "Microsoft.DotNet.Arcade.Sdk": "5.0.0-beta.20224.11" } } diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/GeneratedDemo.csproj b/samples/CSharp/SourceGenerators/GeneratedDemo/GeneratedDemo.csproj new file mode 100644 index 0000000000..dce2692064 --- /dev/null +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/GeneratedDemo.csproj @@ -0,0 +1,24 @@ + + + + Exe + netcoreapp3.1 + preview + + + + + + Always + + + + + + + + + + + + diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings b/samples/CSharp/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings new file mode 100644 index 0000000000..457d6b508d --- /dev/null +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/MainSettings.xmlsettings @@ -0,0 +1,5 @@ + + + false + 1234 + \ No newline at end of file diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs new file mode 100644 index 0000000000..56ee7a8019 --- /dev/null +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/Program.cs @@ -0,0 +1,20 @@ +using System; + +namespace GeneratedDemo +{ + class Program + { + static void Main(string[] args) + { + // Run the various scenarios + Console.WriteLine("Running HelloWorld:\n"); + UseHelloWorldGenerator.Run(); + + Console.WriteLine("\n\nRunning AutoNotify:\n"); + UseAutoNotifyGenerator.Run(); + + Console.WriteLine("\n\nRunning XmlSettings:\n"); + UseXmlSettingsGenerator.Run(); + } + } +} diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs new file mode 100644 index 0000000000..77e346c9a8 --- /dev/null +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseAutoNotifyGenerator.cs @@ -0,0 +1,39 @@ +using System; +using AutoNotify; + +namespace GeneratedDemo +{ + // The view model we'd like to augment + public partial class ExampleViewModel + { + [AutoNotify] + private string _text = "private field text"; + + [AutoNotify(PropertyName = "Count")] + private int _amount = 5; + } + + public static class UseAutoNotifyGenerator + { + public static void Run() + { + ExampleViewModel vm = new ExampleViewModel(); + + // we didn't explicitly create the 'Text' property, it was generated for us + string text = vm.Text; + Console.WriteLine($"Text = {text}"); + + // Properties can have differnt names generated based on the PropertyName argument of the attribute + int count = vm.Count; + Console.WriteLine($"Count = {count}"); + + // the viewmodel will automatically implement INotifyPropertyChanged + vm.PropertyChanged += (o, e) => Console.WriteLine($"Property {e.PropertyName} was changed"); + vm.Text = "abc"; + vm.Count = 123; + + // Try adding fields to the ExampleViewModel class above and tagging them with the [AutoNotify] attribute + // You'll see the matching generated properties visibile in IntelliSense in realtime + } + } +} diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs new file mode 100644 index 0000000000..000328660c --- /dev/null +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseHelloWorldGenerator.cs @@ -0,0 +1,11 @@ +namespace GeneratedDemo +{ + public static class UseHelloWorldGenerator + { + public static void Run() + { + // The static call below is generated at build time, and will list the syntax trees used in the compilation + HelloWorldGenerated.HelloWorld.SayHello(); + } + } +} diff --git a/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs b/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs new file mode 100644 index 0000000000..213181e514 --- /dev/null +++ b/samples/CSharp/SourceGenerators/GeneratedDemo/UseXmlSettingsGenerator.cs @@ -0,0 +1,27 @@ +using System; +using AutoSettings; + +namespace GeneratedDemo +{ + public static class UseXmlSettingsGenerator + { + public static void Run() + { + // This XmlSettings generator makes a static property in the XmlSettings class for each .xmlsettings file + + // here we have the 'Main' settings file from MainSettings.xmlsettings + // the name is determined by the 'name' attribute of the root settings element + XmlSettings.MainSettings main = XmlSettings.Main; + Console.WriteLine($"Reading settings from {main.GetLocation()}"); + + // settings are strongly typed and can be read directly from the static instance + bool firstRun = XmlSettings.Main.FirstRun; + Console.WriteLine($"Setting firstRun = {firstRun}"); + + int cacheSize = XmlSettings.Main.CacheSize; + Console.WriteLine($"Setting cacheSize = {cacheSize}"); + + // Try adding some keys to the settings file and see the settings become available to read from + } + } +} diff --git a/samples/CSharp/SourceGenerators/README.md b/samples/CSharp/SourceGenerators/README.md new file mode 100644 index 0000000000..56976224ad --- /dev/null +++ b/samples/CSharp/SourceGenerators/README.md @@ -0,0 +1,35 @@ +🚧 Work In Progress +======== + +These samples are for an in-progress feature of Roslyn. As such they may change or break as the feature is developed, and no level of support is implied. + +For more infomation on the Source Generators feature, see the [design document](https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md). + +Prerequisites +----- + +These samples require Visual Studio 16.6 or higher. + +Building the samples +----- +Open `SourceGenerators.sln` in Visual Studio or run `dotnet build` from the `\SourceGenerators` directory. + +Running the samples +----- + +The generators must be run as part of another build, as they inject source into the project being built. This repo contains a sample project `GeneratorDemo` that relies of the sample generators to add code to it's compilation. + +Run `GeneratedDemo` in Visual studio or run `dotnet run` from the `GeneratorDemo` directory. + +Using the samples in your project +----- + +You can add the sample generators to your own project by adding an item group containing an analyzer reference: + +```xml + + + +``` + +You may need to close and reopen the solution in Visual Studio for the change to take effect. diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs new file mode 100644 index 0000000000..94be4a9c8d --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/AutoNotifyGenerator.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace Analyzer1 +{ + [Generator] + public class AutoNotifyGenerator : ISourceGenerator + { + private const string attributeText = @" +using System; +namespace AutoNotify +{ + [AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] + sealed class AutoNotifyAttribute : Attribute + { + public AutoNotifyAttribute() + { + } + public string PropertyName { get; set; } + } +} +"; + + public void Initialize(InitializationContext context) + { + // Register a syntax receiver that will be created for each generation pass + context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + } + + public void Execute(SourceGeneratorContext context) + { + // add the attribute text + context.AddSource("AutoNotifyAttribute", SourceText.From(attributeText, Encoding.UTF8)); + + // retreive the populated receiver + if (!(context.SyntaxReceiver is SyntaxReceiver receiver)) + return; + + // we're going to create a new compilation that contains the attribute. + // TODO: we should allow source generators to provide source during initialize, so that this step isn't required. + CSharpParseOptions options = (context.Compilation as CSharpCompilation).SyntaxTrees[0].Options as CSharpParseOptions; + Compilation compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(attributeText, Encoding.UTF8), options)); + + // get the newly bound attribute, and INotifyPropertyChanged + INamedTypeSymbol attributeSymbol = compilation.GetTypeByMetadataName("AutoNotify.AutoNotifyAttribute"); + INamedTypeSymbol notifySymbol = compilation.GetTypeByMetadataName("System.ComponentModel.INotifyPropertyChanged"); + + // loop over the candidate fields, and keep the ones that are actually annotated + List fieldSymbols = new List(); + foreach (FieldDeclarationSyntax field in receiver.CandidateFields) + { + SemanticModel model = compilation.GetSemanticModel(field.SyntaxTree); + foreach (VariableDeclaratorSyntax variable in field.Declaration.Variables) + { + // Get the symbol being decleared by the field, and keep it if its annotated + IFieldSymbol fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol; + if (fieldSymbol.GetAttributes().Any(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default))) + { + fieldSymbols.Add(fieldSymbol); + } + } + } + + // group the fields by class, and generate the source + foreach (IGrouping group in fieldSymbols.GroupBy(f => f.ContainingType)) + { + string classSource = ProcessClass(group.Key, group.ToList(), attributeSymbol, notifySymbol, context); + context.AddSource($"{group.Key.Name}_autoNotify.cs", SourceText.From(classSource, Encoding.UTF8)); + } + } + + private string ProcessClass(INamedTypeSymbol classSymbol, List fields, ISymbol attributeSymbol, ISymbol notifySymbol, SourceGeneratorContext context) + { + if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default)) + { + return null; //TODO: issue a diagnostic that it must be top level + } + + string namespaceName = classSymbol.ContainingNamespace.ToDisplayString(); + + // begin building the generated source + StringBuilder source = new StringBuilder($@" +namespace {namespaceName} +{{ + public partial class {classSymbol.Name} : {notifySymbol.ToDisplayString()} + {{ +"); + + // if the class doesn't implement INotifyPropertyChanged already, add it + if (!classSymbol.Interfaces.Contains(notifySymbol)) + { + source.Append("public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;"); + } + + // create properties for each field + foreach (IFieldSymbol fieldSymbol in fields) + { + ProcessField(source, fieldSymbol, attributeSymbol); + } + + source.Append("} }"); + return source.ToString(); + } + + private void ProcessField(StringBuilder source, IFieldSymbol fieldSymbol, ISymbol attributeSymbol) + { + // get the name and type of the field + string fieldName = fieldSymbol.Name; + ITypeSymbol fieldType = fieldSymbol.Type; + + // get the AutoNotify attribute from the field, and any associated data + AttributeData attributeData = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)); + TypedConstant overridenNameOpt = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "PropertyName").Value; + + string propertyName = chooseName(fieldName, overridenNameOpt); + if (propertyName.Length == 0 || propertyName == fieldName) + { + //TODO: issue a diagnostic that we can't process this field + return; + } + + source.Append($@" +public {fieldType} {propertyName} +{{ + get + {{ + return this.{fieldName}; + }} + + set + {{ + this.{fieldName} = value; + this.PropertyChanged?.Invoke(this, new System.ComponentModel.PropertyChangedEventArgs(nameof({propertyName}))); + }} +}} + +"); + + string chooseName(string fieldName, TypedConstant overridenNameOpt) + { + if (!overridenNameOpt.IsNull) + { + return overridenNameOpt.Value.ToString(); + } + + fieldName = fieldName.TrimStart('_'); + if (fieldName.Length == 0) + return string.Empty; + + if (fieldName.Length == 1) + return fieldName.ToUpper(); + + return fieldName.Substring(0, 1).ToUpper() + fieldName.Substring(1); + } + + } + + /// + /// Created on demand before each generation pass + /// + class SyntaxReceiver : ISyntaxReceiver + { + public List CandidateFields { get; } = new List(); + + /// + /// Called for every syntax node in the compilation, we can inspect the nodes and save any information useful for generation + /// + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + // any field with at least one attribute is a candidate for property generation + if (syntaxNode is FieldDeclarationSyntax fieldDeclarationSyntax + && fieldDeclarationSyntax.AttributeLists.Count > 0) + { + CandidateFields.Add(fieldDeclarationSyntax); + } + } + } + } +} diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs new file mode 100644 index 0000000000..2beb0aebe4 --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/HelloWorldGenerator.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; + +namespace SourceGeneratorSamples +{ + [Generator] + public class HelloWorldGenerator : ISourceGenerator + { + public void Execute(SourceGeneratorContext context) + { + // begin creating the source we'll inject into the users compilation + StringBuilder sourceBuilder = new StringBuilder(@" +using System; +namespace HelloWorldGenerated +{ + public static class HelloWorld + { + public static void SayHello() + { + Console.WriteLine(""Hello from generated code!""); + Console.WriteLine(""The following syntax trees existed in the compilation that created this program:""); +"); + + // using the context, get a list of syntax trees in the users compilation + IEnumerable syntaxTrees = context.Compilation.SyntaxTrees; + + // add the filepath of each tree to the class we're building + foreach (SyntaxTree tree in syntaxTrees) + { + sourceBuilder.AppendLine($@"Console.WriteLine(@"" - {tree.FilePath}"");"); + } + + // finish creating the source to inject + sourceBuilder.Append(@" + } + } +}"); + + // inject the created source into the users compilation + context.AddSource("helloWorldGenerated", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8)); + } + + public void Initialize(InitializationContext context) + { + // No initialization required + } + } +} diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs new file mode 100644 index 0000000000..e277d2cf84 --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SettingsXmlGenerator.cs @@ -0,0 +1,108 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Xml; + +namespace Analyzer1 +{ + [Generator] + public class SettingsXmlGenerator : ISourceGenerator + { + private const string SettingsFileString = @" +namespace XmlSettings +{ + public partial class XmlSettings + { + + } +} +"; + public void Execute(SourceGeneratorContext context) + { + // Using the context, get any additional files that end in .xmlsettings + IEnumerable settingsFiles = context.AdditionalFiles.Where(at => at.Path.EndsWith(".xmlsettings")); + foreach (AdditionalText settingsFile in settingsFiles) + { + ProcessSettingsFile(settingsFile, context); + } + } + + private void ProcessSettingsFile(AdditionalText xmlFile, SourceGeneratorContext context) + { + // try and load the settings file + XmlDocument xmlDoc = new XmlDocument(); + string text = xmlFile.GetText(context.CancellationToken).ToString(); + try + { + xmlDoc.LoadXml(text); + } + catch + { + //TODO: issue a diagnostic that says we couldn't parse it + return; + } + + + // create a class in the XmlSetting class that represnts this entry, and a static field that contains a singleton instance. + string fileName = Path.GetFileName(xmlFile.Path); + string name = xmlDoc.DocumentElement.GetAttribute("name"); + + StringBuilder sb = new StringBuilder($@" +namespace AutoSettings +{{ + using System; + using System.Xml; + + public partial class XmlSettings + {{ + + public static {name}Settings {name} {{ get; }} = new {name}Settings(""{fileName}""); + + public class {name}Settings + {{ + + XmlDocument xmlDoc = new XmlDocument(); + + private string fileName; + + public string GetLocation() => fileName; + + internal {name}Settings(string fileName) + {{ + this.fileName = fileName; + xmlDoc.Load(fileName); + }} +"); + + for(int i = 0; i < xmlDoc.DocumentElement.ChildNodes.Count; i++) + { + XmlElement setting = (XmlElement)xmlDoc.DocumentElement.ChildNodes[i]; + string settingName = setting.GetAttribute("name"); + string settingType = setting.GetAttribute("type"); + + sb.Append($@" + +public {settingType} {settingName} +{{ + get + {{ + return ({settingType}) Convert.ChangeType(((XmlElement)xmlDoc.DocumentElement.ChildNodes[{i}]).InnerText, typeof({settingType})); + }} +}} +"); + } + + sb.Append("} } }"); + + context.AddSource($"Settings_{name}", SourceText.From(sb.ToString(), Encoding.UTF8)); + } + + public void Initialize(InitializationContext context) + { + } + } +} diff --git a/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.csproj b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.csproj new file mode 100644 index 0000000000..e66e22427d --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGeneratorSamples/SourceGeneratorSamples.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + 8.0 + + + + https://dotnet.myget.org/F/roslyn/api/v3/index.json ;$(RestoreAdditionalProjectSources) + + + + + + + + \ No newline at end of file diff --git a/samples/CSharp/SourceGenerators/SourceGenerators.sln b/samples/CSharp/SourceGenerators/SourceGenerators.sln new file mode 100644 index 0000000000..1e5b737dca --- /dev/null +++ b/samples/CSharp/SourceGenerators/SourceGenerators.sln @@ -0,0 +1,31 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30022.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorSamples", "SourceGeneratorSamples\SourceGeneratorSamples.csproj", "{B452269D-856C-4FE6-8900-3D81461AF864}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GeneratedDemo", "GeneratedDemo\GeneratedDemo.csproj", "{DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B452269D-856C-4FE6-8900-3D81461AF864}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B452269D-856C-4FE6-8900-3D81461AF864}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B452269D-856C-4FE6-8900-3D81461AF864}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B452269D-856C-4FE6-8900-3D81461AF864}.Release|Any CPU.Build.0 = Release|Any CPU + {DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB138C8B-7C34-4AC7-A443-A0B29D1CE8A5}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {623C84B6-B8A4-4F29-8E68-4ED37D4529D5} + EndGlobalSection +EndGlobal