diff --git a/.nuget/nuget.config b/.nuget/nuget.config
new file mode 100644
index 000000000..da0fdaeeb
--- /dev/null
+++ b/.nuget/nuget.config
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 000000000..eba65de8f
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,29 @@
+{
+ // Controls if the editor shows reference information for the modes that support it
+ "editor.referenceInfos": false,
+
+ // When opening a file, `editor.tabSize` and `editor.insertSpaces` will be detected based on the file contents.
+ "editor.detectIndentation": false,
+
+ // Typescript version from local package to be consistent
+ "typescript.tsdk": "node_modules/typescript/lib",
+
+ // Configure glob patterns for excluding files and folders.
+ "files.exclude": {
+ "_test-output": true,
+ "**/node_modules": true,
+ "**/artifacts": true,
+ "**/build": true,
+ "**/out": true,
+ "**/obj": true,
+ "**/bin": true,
+ "**/*.lock.json": true,
+ "**/*.bat": true,
+ "**/*.sln": true,
+ "**/*.sln.DotSettings": true,
+ "**/*.user": true,
+ "**/*.xproj": true,
+ "**/*.gitattributes": true,
+ ".vs:": true
+ }
+}
\ No newline at end of file
diff --git a/PinkParrot.sln b/PinkParrot.sln
new file mode 100644
index 000000000..6296fae50
--- /dev/null
+++ b/PinkParrot.sln
@@ -0,0 +1,82 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25420.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{24A3171D-2905-49C9-8A49-A473799014E8}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4B5539DB-F68E-4DBA-B22A-72B9FE7FE2D8}"
+ ProjectSection(SolutionItems) = preProject
+ global.json = global.json
+ EndProjectSection
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot", "src\PinkParrot\PinkParrot.xproj", "{61F6BBCE-A080-4400-B194-70E2F5D2096E}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pinkparrot_infrastructure", "pinkparrot_infrastructure", "{8CF53B92-5EB1-461D-98F8-70DA9B603FBF}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pinkparrot_core", "pinkparrot_core", "{4C6B06C2-6D77-4E0E-AE32-D7050236433A}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot.Core.Tests", "src\pinkparrot_core\PinkParrot.Core.Tests\PinkParrot.Core.Tests.xproj", "{4A27B9DE-F553-4A82-B866-A29EF8A5A0AF}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot.Core", "src\pinkparrot_core\PinkParrot.Core\PinkParrot.Core.xproj", "{47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot.Infrastructure.Tests", "src\pinkparrot_infrastructure\PinkParrot.Infrastructure.Tests\PinkParrot.Infrastructure.Tests.xproj", "{840C02B1-48F8-4C8A-8862-8A3FDEFDE8D5}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot.Infrastructure", "src\pinkparrot_infrastructure\PinkParrot.Infrastructure\PinkParrot.Infrastructure.xproj", "{BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pinkparrot_events", "pinkparrot_events", "{6AE39761-FD74-45CD-99CF-73D3D2E5D064}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot.Events", "src\pinkparrot_events\PinkParrot.Events\PinkParrot.Events.xproj", "{25F66C64-058A-4D44-BC0C-F12A054F9A91}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "pinkparrot_write", "pinkparrot_write", "{4AED438F-684F-4FAE-B016-21CF2EAEA79F}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "PinkParrot.Write", "src\pinkparrot_write\PinkParrot.Write\PinkParrot.Write.xproj", "{A85201C6-6AF8-4B63-8365-08F741050438}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {61F6BBCE-A080-4400-B194-70E2F5D2096E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4A27B9DE-F553-4A82-B866-A29EF8A5A0AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4A27B9DE-F553-4A82-B866-A29EF8A5A0AF}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4A27B9DE-F553-4A82-B866-A29EF8A5A0AF}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4A27B9DE-F553-4A82-B866-A29EF8A5A0AF}.Release|Any CPU.Build.0 = Release|Any CPU
+ {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {840C02B1-48F8-4C8A-8862-8A3FDEFDE8D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {840C02B1-48F8-4C8A-8862-8A3FDEFDE8D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {840C02B1-48F8-4C8A-8862-8A3FDEFDE8D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {840C02B1-48F8-4C8A-8862-8A3FDEFDE8D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {25F66C64-058A-4D44-BC0C-F12A054F9A91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A85201C6-6AF8-4B63-8365-08F741050438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A85201C6-6AF8-4B63-8365-08F741050438}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A85201C6-6AF8-4B63-8365-08F741050438}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A85201C6-6AF8-4B63-8365-08F741050438}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {61F6BBCE-A080-4400-B194-70E2F5D2096E} = {24A3171D-2905-49C9-8A49-A473799014E8}
+ {4A27B9DE-F553-4A82-B866-A29EF8A5A0AF} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {47F3C27E-698B-4EDF-A7E8-D7F4232AFBB0} = {4C6B06C2-6D77-4E0E-AE32-D7050236433A}
+ {840C02B1-48F8-4C8A-8862-8A3FDEFDE8D5} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
+ {BD1C30A8-8FFA-4A92-A9BD-B67B1CDDD84C} = {8CF53B92-5EB1-461D-98F8-70DA9B603FBF}
+ {25F66C64-058A-4D44-BC0C-F12A054F9A91} = {6AE39761-FD74-45CD-99CF-73D3D2E5D064}
+ {A85201C6-6AF8-4B63-8365-08F741050438} = {4AED438F-684F-4FAE-B016-21CF2EAEA79F}
+ EndGlobalSection
+EndGlobal
diff --git a/PinkParrot.sln.DotSettings b/PinkParrot.sln.DotSettings
new file mode 100644
index 000000000..44e2de54f
--- /dev/null
+++ b/PinkParrot.sln.DotSettings
@@ -0,0 +1,84 @@
+
+ False
+ True
+ False
+ False
+ True
+ True
+ True
+ True
+ True
+ False
+ True
+
+
+ ExplicitlyExcluded
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+
+
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ DO_NOT_SHOW
+ SUGGESTION
+ SUGGESTION
+ SUGGESTION
+ DO_NOT_SHOW
+ TypeScript16
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Namespaces"><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSUpdateFileHeader>True</CSUpdateFileHeader></Profile>
+ <?xml version="1.0" encoding="utf-16"?><Profile name="Typescript"><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs></Profile>
+
+ SingleQuoted
+ ==========================================================================
+ $FILENAME$
+ PinkParrot Headless CMS
+==========================================================================
+ Copyright (c) PinkParrot Group
+ All rights reserved.
+==========================================================================
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" />
+ True
+ True
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 000000000..60ab9b437
--- /dev/null
+++ b/global.json
@@ -0,0 +1,6 @@
+{
+ "projects": [ "src" ],
+ "sdk": {
+ "version": "1.0.0-preview2-003121"
+ }
+}
diff --git a/src/PinkParrot/PinkParrot.xproj b/src/PinkParrot/PinkParrot.xproj
new file mode 100644
index 000000000..f525b6954
--- /dev/null
+++ b/src/PinkParrot/PinkParrot.xproj
@@ -0,0 +1,25 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+ 61f6bbce-a080-4400-b194-70e2f5d2096e
+ PinkParrot
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+
+ 2.0
+
+
+
+
+
+
+
diff --git a/src/PinkParrot/Program.cs b/src/PinkParrot/Program.cs
new file mode 100644
index 000000000..7f88eb8b5
--- /dev/null
+++ b/src/PinkParrot/Program.cs
@@ -0,0 +1,28 @@
+// ==========================================================================
+// Program.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System.IO;
+using Microsoft.AspNetCore.Hosting;
+
+namespace PinkParrot
+{
+ public class Program
+ {
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseKestrel()
+ .UseContentRoot(Directory.GetCurrentDirectory())
+ .UseIISIntegration()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/src/PinkParrot/Project_Readme.html b/src/PinkParrot/Project_Readme.html
new file mode 100644
index 000000000..1a0f5b51a
--- /dev/null
+++ b/src/PinkParrot/Project_Readme.html
@@ -0,0 +1,187 @@
+
+
+
+
+ Welcome to ASP.NET Core
+
+
+
+
+
+
+
+
+
This application consists of:
+
+ - Sample pages using ASP.NET Core MVC
+ - Bower for managing client-side libraries
+ - Theming using Bootstrap
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/PinkParrot/Properties/launchSettings.json b/src/PinkParrot/Properties/launchSettings.json
new file mode 100644
index 000000000..a19c657ab
--- /dev/null
+++ b/src/PinkParrot/Properties/launchSettings.json
@@ -0,0 +1,27 @@
+{
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:65351/",
+ "sslPort": 0
+ }
+ },
+ "profiles": {
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "PinkParrot": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "http://localhost:5000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/PinkParrot/Startup.cs b/src/PinkParrot/Startup.cs
new file mode 100644
index 000000000..3bcbdb1a0
--- /dev/null
+++ b/src/PinkParrot/Startup.cs
@@ -0,0 +1,41 @@
+// ==========================================================================
+// Startup.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+
+namespace PinkParrot
+{
+ public class Startup
+ {
+ // This method gets called by the runtime. Use this method to add services to the container.
+ // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
+ public void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
+ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
+ {
+ loggerFactory.AddConsole();
+
+ if (env.IsDevelopment())
+ {
+ app.UseDeveloperExceptionPage();
+ }
+
+ app.Run(async (context) =>
+ {
+ await context.Response.WriteAsync("Hello World!");
+ });
+ }
+ }
+}
diff --git a/src/PinkParrot/project.json b/src/PinkParrot/project.json
new file mode 100644
index 000000000..361cfdacb
--- /dev/null
+++ b/src/PinkParrot/project.json
@@ -0,0 +1,48 @@
+{
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0",
+ "type": "platform"
+ },
+ "Microsoft.AspNetCore.Diagnostics": "1.0.0",
+
+ "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0",
+ "Microsoft.AspNetCore.Server.Kestrel": "1.0.0",
+ "Microsoft.Extensions.Logging.Console": "1.0.0"
+ },
+
+ "tools": {
+ "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-preview2-final"
+ },
+
+ "frameworks": {
+ "netcoreapp1.0": {
+ "imports": [
+ "dotnet5.6",
+ "portable-net45+win8"
+ ]
+ }
+ },
+
+ "buildOptions": {
+ "emitEntryPoint": true,
+ "preserveCompilationContext": true
+ },
+
+ "runtimeOptions": {
+ "configProperties": {
+ "System.GC.Server": true
+ }
+ },
+
+ "publishOptions": {
+ "include": [
+ "wwwroot",
+ "web.config"
+ ]
+ },
+
+ "scripts": {
+ "postpublish": [ "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" ]
+ }
+}
diff --git a/src/PinkParrot/web.config b/src/PinkParrot/web.config
new file mode 100644
index 000000000..dc0514fca
--- /dev/null
+++ b/src/PinkParrot/web.config
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pinkparrot_core/PinkParrot.Core.Tests/PinkParrot.Core.Tests.xproj b/src/pinkparrot_core/PinkParrot.Core.Tests/PinkParrot.Core.Tests.xproj
new file mode 100644
index 000000000..1290e1f9d
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core.Tests/PinkParrot.Core.Tests.xproj
@@ -0,0 +1,19 @@
+
+
+
+ 14.0.25420
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 4a27b9de-f553-4a82-b866-a29ef8a5a0af
+ PinkParrot.Core.Tests
+ .\obj
+ .\bin\
+
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/pinkparrot_core/PinkParrot.Core.Tests/project.json b/src/pinkparrot_core/PinkParrot.Core.Tests/project.json
new file mode 100644
index 000000000..d06272be5
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core.Tests/project.json
@@ -0,0 +1,29 @@
+{
+ "version": "1.0.0-*",
+ "testRunner": "xunit",
+ "dependencies": {
+ "dotnet-test-xunit": "2.2.0-preview2-build1029",
+ "PinkParrot.Core": "1.0.0-*",
+ "xunit": "2.2.0-beta2-build3300"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.0.0"
+ }
+ }
+ }
+ },
+ "buildOptions": {
+ "copyToOutput": {
+ "include": [
+ "xunit.runner.json"
+ ]
+ }
+ },
+ "tooling": {
+ "defaultNamespace": "PinkParrot.Core.Tests"
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_core/PinkParrot.Core.Tests/xunit.runner.json b/src/pinkparrot_core/PinkParrot.Core.Tests/xunit.runner.json
new file mode 100644
index 000000000..6b3f1f87d
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core.Tests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+ "diagnosticMessages": false,
+ "methodDisplay": "classAndMethod",
+ "parallelizeTestCollections": true
+}
diff --git a/src/pinkparrot_core/PinkParrot.Core/PinkParrot.Core.xproj b/src/pinkparrot_core/PinkParrot.Core/PinkParrot.Core.xproj
new file mode 100644
index 000000000..8d0554b61
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/PinkParrot.Core.xproj
@@ -0,0 +1,19 @@
+
+
+
+ 14.0.25420
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 47f3c27e-698b-4edf-a7e8-d7f4232afbb0
+ PinkParrot.Core
+ .\obj
+ .\bin\
+
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/pinkparrot_core/PinkParrot.Core/Schema/ModelField.cs b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelField.cs
new file mode 100644
index 000000000..816f6b296
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelField.cs
@@ -0,0 +1,200 @@
+// ==========================================================================
+// ModelField.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using PinkParrot.Infrastructure;
+using PinkParrot.Infrastructure.Tasks;
+
+// ReSharper disable ConvertIfStatementToReturnStatement
+
+namespace PinkParrot.Core.Schema
+{
+ public abstract class ModelField
+ {
+ private readonly Guid id;
+ private string name;
+ private string hint;
+ private string displayName;
+ private bool isRequired;
+ private bool isDisabled;
+ private bool isHidden;
+
+ public Guid Id
+ {
+ get { return id; }
+ }
+
+ public string Name
+ {
+ get { return name; }
+ }
+
+ public string Hint
+ {
+ get { return hint; }
+ }
+
+ public string DisplayName
+ {
+ get { return displayName; }
+ }
+
+ public bool IsRequired
+ {
+ get { return isRequired; }
+ }
+
+ public bool IsHidden
+ {
+ get { return isHidden; }
+ }
+
+ public bool IsDisabled
+ {
+ get { return isDisabled; }
+ }
+
+ protected ModelField(Guid id, string name)
+ {
+ Guard.NotEmpty(id, nameof(id));
+ Guard.ValidSlug(name, nameof(name));
+
+ this.id = id;
+
+ this.name = name;
+ }
+
+ public ModelField Configure(PropertiesBag settings, ICollection errors)
+ {
+ var clone = Clone();
+
+ if (settings.Contains("Name"))
+ {
+ clone.name = settings["Name"].ToString();
+
+ if (!clone.name.IsSlug())
+ {
+ errors.Add("Field name must be a slug");
+ }
+ }
+
+ if (settings.Contains("Hint"))
+ {
+ clone.hint = settings["Hint"].ToString()?.Trim() ?? string.Empty;
+ }
+
+ if (settings.Contains("DisplayName"))
+ {
+ clone.displayName = settings["DisplayName"].ToString()?.Trim() ?? string.Empty;
+ }
+
+ if (settings.Contains("IsRequired"))
+ {
+ try
+ {
+ clone.isRequired = settings["IsRequired"].ToBoolean(CultureInfo.InvariantCulture);
+ }
+ catch (InvalidCastException)
+ {
+ errors.Add("IsRequired is not a valid boolean");
+ }
+ }
+
+ clone.ConfigureCore(settings, errors);
+
+ return clone;
+ }
+
+ protected virtual void ConfigureCore(PropertiesBag settings, ICollection errors)
+ {
+ }
+
+ public Task ValidateAsync(PropertyValue property, ICollection errors)
+ {
+ Guard.NotNull(property, nameof(property));
+
+ if (isRequired && property.RawValue == null)
+ {
+ errors.Add(" is required");
+ }
+
+ if (property.RawValue == null)
+ {
+ return TaskHelper.Done;
+ }
+
+ return ValidateCoreAsync(property, errors);
+ }
+
+ protected virtual Task ValidateCoreAsync(PropertyValue property, ICollection errors)
+ {
+ return TaskHelper.Done;
+ }
+
+ public ModelField Hide()
+ {
+ if (isHidden)
+ {
+ throw new DomainValidationException($"The field '{name} is already hidden.");
+ }
+
+ var clone = Clone();
+
+ clone.isHidden = true;
+
+ return Clone();
+ }
+
+ public ModelField Show()
+ {
+ if (!isHidden)
+ {
+ throw new DomainValidationException($"The field '{name} is already visible.");
+ }
+
+ var clone = Clone();
+
+ clone.isHidden = false;
+
+ return Clone();
+ }
+
+ public ModelField Disable()
+ {
+ if (isDisabled)
+ {
+ throw new DomainValidationException($"The field '{name} is already disabled.");
+ }
+
+ var clone = Clone();
+
+ clone.isDisabled = true;
+
+ return clone;
+ }
+
+ public ModelField Enable()
+ {
+ if (!isDisabled)
+ {
+ throw new DomainValidationException($"The field '{name} is already enabled.");
+ }
+
+ var clone = Clone();
+
+ clone.isDisabled = false;
+
+ return clone;
+ }
+
+ protected abstract ModelField Clone();
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_core/PinkParrot.Core/Schema/ModelFieldFactory.cs b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelFieldFactory.cs
new file mode 100644
index 000000000..e318249d9
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelFieldFactory.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// ModelFieldFactory.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+
+namespace PinkParrot.Core.Schema
+{
+ public class ModelFieldFactory
+ {
+ public virtual ModelField CreateField(Guid id, string type, string fieldName)
+ {
+ return new NumberField(id, fieldName);
+ }
+ }
+}
+
diff --git a/src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchema.cs b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchema.cs
new file mode 100644
index 000000000..cf46324ee
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchema.cs
@@ -0,0 +1,177 @@
+// ==========================================================================
+// ModelSchema.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Linq;
+using System.Threading.Tasks;
+using PinkParrot.Infrastructure;
+
+namespace PinkParrot.Core.Schema
+{
+ public sealed class ModelSchema
+ {
+ private readonly ModelSchemaMetadata metadata;
+ private readonly ImmutableDictionary fields;
+ private readonly Dictionary fieldsByName;
+
+ public ModelSchema(ModelSchemaMetadata metadata, ImmutableDictionary fields)
+ {
+ Guard.NotNull(fields, nameof(fields));
+ Guard.NotNull(metadata, nameof(metadata));
+
+ this.fields = fields;
+
+ this.metadata = metadata;
+
+ fieldsByName = fields.Values.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public static ModelSchema Create(string name)
+ {
+ if (!name.IsSlug())
+ {
+ throw new DomainValidationException("Cannot create the schema.", $"'{name}' is not a valid slug.");
+ }
+
+ return new ModelSchema(new ModelSchemaMetadata(name), ImmutableDictionary.Empty);
+ }
+
+ public IReadOnlyDictionary Fields
+ {
+ get { return fields; }
+ }
+
+ public ModelSchemaMetadata Metadata
+ {
+ get { return metadata; }
+ }
+
+ public ModelSchema Update(ModelSchemaMetadata newMetadata)
+ {
+ Guard.NotNull(newMetadata, nameof(newMetadata));
+
+ return new ModelSchema(newMetadata, fields);
+ }
+
+ public ModelSchema AddField(Guid id, string type, string fieldName, ModelFieldFactory factory)
+ {
+ var field = factory.CreateField(id, type, fieldName);
+
+ return SetField(field);
+ }
+
+ public ModelSchema SetField(Guid fieldId, PropertiesBag settings)
+ {
+ Guard.NotNull(settings, nameof(settings));
+
+ return UpdateField(fieldId, field =>
+ {
+ var errors = new List();
+
+ var newField = field.Configure(settings, errors);
+
+ if (errors.Any())
+ {
+ throw new DomainValidationException($"Cannot update field with id '{fieldId}', becase the settings are invalid.", errors);
+ }
+
+ return newField;
+ });
+ }
+
+ public ModelSchema DisableField(Guid fieldId)
+ {
+ return UpdateField(fieldId, field => field.Disable());
+ }
+
+ public ModelSchema EnableField(Guid fieldId)
+ {
+ return UpdateField(fieldId, field => field.Enable());
+ }
+
+ public ModelSchema HideField(Guid fieldId)
+ {
+ return UpdateField(fieldId, field => field.Show());
+ }
+
+ public ModelSchema ShowField(Guid fieldId)
+ {
+ return UpdateField(fieldId, field => field.Show());
+ }
+
+ public ModelSchema SetField(ModelField field)
+ {
+ Guard.NotNull(field, nameof(field));
+
+ if (fields.Values.Any(f => f.Name == field.Name && f.Id != field.Id))
+ {
+ throw new DomainValidationException($"A field with name '{field.Name}' already exists.");
+ }
+
+ return new ModelSchema(metadata, fields.SetItem(field.Id, field));
+ }
+
+ public ModelSchema DeleteField(Guid fieldId)
+ {
+ Guard.NotEmpty(fieldId, nameof(fieldId));
+
+ if (!fields.ContainsKey(fieldId))
+ {
+ throw new DomainValidationException($"A field with id {fieldId} does not exist.");
+ }
+
+ return new ModelSchema(metadata, fields.Remove(fieldId));
+ }
+
+ private ModelSchema UpdateField(Guid fieldId, Func updater)
+ {
+ ModelField field;
+
+ if (!fields.TryGetValue(fieldId, out field))
+ {
+ throw new DomainValidationException($"Cannot update field with id '{fieldId}'.", "Field does not exist.");
+ }
+
+ var newField = updater(field);
+
+ return SetField(newField);
+ }
+
+ public async Task ValidateAsync(PropertiesBag data)
+ {
+ Guard.NotNull(data, nameof(data));
+
+ var errors = new List();
+
+ foreach (var kvp in data.Properties)
+ {
+ ModelField field;
+
+ if (fieldsByName.TryGetValue(kvp.Key, out field))
+ {
+ var newErrors = new List();
+
+ await field.ValidateAsync(kvp.Value, newErrors);
+
+ errors.AddRange(newErrors.Select(e => e.Replace("", "'" + field.Name + "'")));
+ }
+ else
+ {
+ errors.Add($"'{kvp.Key}' is not a known field");
+ }
+ }
+
+ if (errors.Any())
+ {
+ throw new DomainValidationException("The data is not valid.", errors);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchemaMetadata.cs b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchemaMetadata.cs
new file mode 100644
index 000000000..63ee493a4
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/Schema/ModelSchemaMetadata.cs
@@ -0,0 +1,81 @@
+// ==========================================================================
+// ModelSchemaMetadata.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using PinkParrot.Infrastructure;
+
+namespace PinkParrot.Core.Schema
+{
+ public sealed class ModelSchemaMetadata
+ {
+ private string name;
+ private string displayName;
+ private string hint;
+ private string itemTitle;
+
+ public string Name
+ {
+ get { return name; }
+ }
+
+ public string DisplayName
+ {
+ get { return displayName; }
+ }
+
+ public string Hint
+ {
+ get { return hint; }
+ }
+
+ public string ItemTitle
+ {
+ get { return itemTitle; }
+ }
+
+ public ModelSchemaMetadata(string name)
+ {
+ Guard.ValidSlug(name, nameof(name));
+
+ this.name = name;
+ }
+
+ public ModelSchemaMetadata Configure(string newName, PropertiesBag properties)
+ {
+ Guard.NotNull(properties, nameof(properties));
+
+ var clone = (ModelSchemaMetadata) MemberwiseClone();
+
+ if (newName != null)
+ {
+ if (!newName.IsSlug())
+ {
+ throw new DomainValidationException("Cannot update the schema.", $"'{newName}' is not a valid slug.");
+ }
+
+ clone.name = newName;
+ }
+
+ if (properties.Contains("Hint"))
+ {
+ clone.hint = properties["Hint"].ToString()?.Trim();
+ }
+
+ if (properties.Contains("DisplayName"))
+ {
+ clone.displayName = properties["DisplayName"].ToString()?.Trim();
+ }
+
+ if (properties.Contains("ItemTitle"))
+ {
+ clone.itemTitle = properties["ItemTitle"].ToString()?.Trim();
+ }
+
+ return clone;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_core/PinkParrot.Core/Schema/NumberField.cs b/src/pinkparrot_core/PinkParrot.Core/Schema/NumberField.cs
new file mode 100644
index 000000000..90d0ad8e1
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/Schema/NumberField.cs
@@ -0,0 +1,95 @@
+// ==========================================================================
+// NumberField.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Threading.Tasks;
+using PinkParrot.Infrastructure;
+using PinkParrot.Infrastructure.Tasks;
+
+namespace PinkParrot.Core.Schema
+{
+ public sealed class NumberField : ModelField
+ {
+ private double? maxValue;
+ private double? minValue;
+
+ public double? MaxValue
+ {
+ get { return maxValue; }
+ }
+
+ public double? MinValue
+ {
+ get { return minValue; }
+ }
+
+ public NumberField(Guid id, string name)
+ : base(id, name)
+ {
+ }
+
+ protected override void ConfigureCore(PropertiesBag settings, ICollection errors)
+ {
+ maxValue = ParseNumber("MaxValue", settings, errors);
+ minValue = ParseNumber("MinValue", settings, errors);
+
+ if (maxValue.HasValue && minValue.HasValue && minValue.Value > maxValue.Value)
+ {
+ errors.Add("MinValue cannot be larger than max value");
+ }
+ }
+
+ private static double? ParseNumber(string key, PropertiesBag settings, ICollection errors)
+ {
+ try
+ {
+ if (settings.Contains(key))
+ {
+ return settings[key].ToNullableDouble(CultureInfo.InvariantCulture);
+ }
+ }
+ catch (InvalidCastException)
+ {
+ errors.Add($"'{key}' is not a valid number");
+ }
+
+ return null;
+ }
+
+ protected override Task ValidateCoreAsync(PropertyValue property, ICollection errors)
+ {
+ try
+ {
+ var value = property.ToDouble(CultureInfo.InvariantCulture);
+
+ if (MinValue.HasValue && value < MinValue.Value)
+ {
+ errors.Add($" must be greater than {MinValue}");
+ }
+
+ if (MaxValue.HasValue && value > MaxValue.Value)
+ {
+ errors.Add($" must be less than {MaxValue}");
+ }
+ }
+ catch (InvalidCastException)
+ {
+ errors.Add(" is not a valid number");
+ }
+
+ return TaskHelper.Done;
+ }
+
+ protected override ModelField Clone()
+ {
+ return (ModelField)MemberwiseClone();
+ }
+ }
+}
diff --git a/src/pinkparrot_core/PinkParrot.Core/project.json b/src/pinkparrot_core/PinkParrot.Core/project.json
new file mode 100644
index 000000000..dc9aa12ab
--- /dev/null
+++ b/src/pinkparrot_core/PinkParrot.Core/project.json
@@ -0,0 +1,26 @@
+{
+ "version": "1.0.0-*",
+
+ "dependencies": {
+ "EventStore.ClientAPI.DotNetCore": "1.0.0",
+ "NETStandard.Library": "1.6.0",
+ "NodaTime": "2.0.0-alpha20160729",
+ "PinkParrot.Infrastructure": "1.0.0-*",
+ "protobuf-net": "2.1.0"
+ },
+
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.0.0"
+ }
+ }
+ }
+ },
+
+ "tooling": {
+ "defaultNamespace": "PinkParrot.Core"
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/.gitignore b/src/pinkparrot_events/PinkParrot.Events/.gitignore
new file mode 100644
index 000000000..0ca27f04e
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/.gitignore
@@ -0,0 +1,234 @@
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+
+# User-specific files
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+build/
+bld/
+[Bb]in/
+[Oo]bj/
+
+# Visual Studio 2015 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUNIT
+*.VisualState.xml
+TestResult.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# DNX
+project.lock.json
+artifacts/
+
+*_i.c
+*_p.c
+*_i.h
+*.ilk
+*.meta
+*.obj
+*.pch
+*.pdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*.log
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# JustCode is a .NET coding add-in
+.JustCode
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# TODO: Comment the next line if you want to checkin your web deploy settings
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# NuGet Packages
+*.nupkg
+# The packages folder can be ignored because of Package Restore
+**/packages/*
+# except build/, which is used as an MSBuild target.
+!**/packages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/packages/repositories.config
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Microsoft Azure ApplicationInsights config file
+ApplicationInsights.config
+
+# Windows Store app package directory
+AppPackages/
+BundleArtifacts/
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.pfx
+*.publishsettings
+node_modules/
+orleans.codegen.cs
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+
+# SQL Server files
+*.mdf
+*.ldf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+
+# FAKE - F# Make
+.fake/
diff --git a/src/pinkparrot_events/PinkParrot.Events/PinkParrot.Events.xproj b/src/pinkparrot_events/PinkParrot.Events/PinkParrot.Events.xproj
new file mode 100644
index 000000000..eb3e71ee7
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/PinkParrot.Events.xproj
@@ -0,0 +1,19 @@
+
+
+
+ 14.0.25420
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 25f66c64-058a-4d44-bc0c-f12a054f9a91
+ PinkParrot.Events
+ .\obj
+ .\bin\
+
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldAdded.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldAdded.cs
new file mode 100644
index 000000000..ca22786d3
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldAdded.cs
@@ -0,0 +1,22 @@
+// ==========================================================================
+// ModelFieldAdded.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldAdded : IEvent
+ {
+ public Guid FieldId { get; set; }
+
+ public string FieldType;
+
+ public string FieldName { get; set; }
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldDeleted.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldDeleted.cs
new file mode 100644
index 000000000..04e5609c6
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldDeleted.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// ModelFieldDeleted.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldDeleted : IEvent
+ {
+ public Guid FieldId;
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldDisabled.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldDisabled.cs
new file mode 100644
index 000000000..cbdaee9a2
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldDisabled.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// ModelFieldDisabled.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldDisabled : IEvent
+ {
+ public Guid FieldId;
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldEnabled.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldEnabled.cs
new file mode 100644
index 000000000..9a8ff9e24
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldEnabled.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// ModelFieldEnabled.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldEnabled : IEvent
+ {
+ public Guid FieldId;
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldHidden.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldHidden.cs
new file mode 100644
index 000000000..fde8b7657
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldHidden.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// ModelFieldHidden.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldHidden : IEvent
+ {
+ public Guid FieldId;
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldShown.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldShown.cs
new file mode 100644
index 000000000..212fe4114
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldShown.cs
@@ -0,0 +1,18 @@
+// ==========================================================================
+// ModelFieldShown.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldShown : IEvent
+ {
+ public Guid FieldId;
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldUpdated.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldUpdated.cs
new file mode 100644
index 000000000..e993c8068
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelFieldUpdated.cs
@@ -0,0 +1,21 @@
+// ==========================================================================
+// ModelFieldUpdated.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using PinkParrot.Infrastructure;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelFieldUpdated : IEvent
+ {
+ public Guid FieldId { get; set; }
+
+ public PropertiesBag Settings { get; set; }
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaCreated.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaCreated.cs
new file mode 100644
index 000000000..9e9d8f8d7
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaCreated.cs
@@ -0,0 +1,17 @@
+// ==========================================================================
+// ModelSchemaCreated.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelSchemaCreated : IEvent
+ {
+ public string Name;
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaDeleted.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaDeleted.cs
new file mode 100644
index 000000000..1671d9501
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaDeleted.cs
@@ -0,0 +1,16 @@
+// ==========================================================================
+// ModelSchemaDeleted.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelSchemaDeleted : IEvent
+ {
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaUpdated.cs b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaUpdated.cs
new file mode 100644
index 000000000..fcaaf5225
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/Schema/ModelSchemaUpdated.cs
@@ -0,0 +1,20 @@
+// ==========================================================================
+// ModelSchemaUpdated.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using PinkParrot.Infrastructure;
+using PinkParrot.Infrastructure.CQRS;
+
+namespace PinkParrot.Events.Schema
+{
+ public class ModelSchemaUpdated : IEvent
+ {
+ public string NewName;
+
+ public PropertiesBag Settings { get; set; }
+ }
+}
diff --git a/src/pinkparrot_events/PinkParrot.Events/project.json b/src/pinkparrot_events/PinkParrot.Events/project.json
new file mode 100644
index 000000000..e676a1907
--- /dev/null
+++ b/src/pinkparrot_events/PinkParrot.Events/project.json
@@ -0,0 +1,24 @@
+{
+ "version": "1.0.0-*",
+
+ "dependencies": {
+ "NETStandard.Library": "1.6.0",
+ "NodaTime": "2.0.0-alpha20160729",
+ "PinkParrot.Infrastructure": "1.0.0-*"
+ },
+
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.0.0"
+ }
+ }
+ }
+ },
+
+ "tooling": {
+ "defaultNamespace": "PinkParrot.Events"
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/CollectionExtensionsTest.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/CollectionExtensionsTest.cs
new file mode 100644
index 000000000..e6f0a62e0
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/CollectionExtensionsTest.cs
@@ -0,0 +1,169 @@
+// ==========================================================================
+// CollectionExtensionsTest.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System.Collections.Generic;
+using Xunit;
+
+namespace PinkParrot.Infrastructure
+{
+ public class CollectionExtensionTest
+ {
+ private readonly Dictionary valueDictionary = new Dictionary();
+ private readonly Dictionary> listDictionary = new Dictionary>();
+
+ [Fact]
+ public void GetOrDefault_should_return_value_if_key_exists()
+ {
+ valueDictionary[12] = 34;
+
+ Assert.Equal(34, valueDictionary.GetOrDefault(12));
+ }
+
+ [Fact]
+ public void GetOrDefault_should_return_default_and_not_add_it_if_key_not_exists()
+ {
+ Assert.Equal(0, valueDictionary.GetOrDefault(12));
+ Assert.False(valueDictionary.ContainsKey(12));
+ }
+
+ [Fact]
+ public void GetOrAddDefault_should_return_value_if_key_exists()
+ {
+ valueDictionary[12] = 34;
+
+ Assert.Equal(34, valueDictionary.GetOrAddDefault(12));
+ }
+
+ [Fact]
+ public void GetOrAddDefault_should_return_default_and_add_it_if_key_not_exists()
+ {
+ Assert.Equal(0, valueDictionary.GetOrAddDefault(12));
+ Assert.Equal(0, valueDictionary[12]);
+ }
+
+ [Fact]
+ public void GetOrCreate_should_return_value_if_key_exists()
+ {
+ valueDictionary[12] = 34;
+
+ Assert.Equal(34, valueDictionary.GetOrCreate(12, x => 34));
+ }
+
+ [Fact]
+ public void GetOrCreate_should_return_default_but_not_add_it_if_key_not_exists()
+ {
+ Assert.Equal(24, valueDictionary.GetOrCreate(12, x => 24));
+ Assert.False(valueDictionary.ContainsKey(12));
+ }
+
+ [Fact]
+ public void GetOrAdd_should_return_value_if_key_exists()
+ {
+ valueDictionary[12] = 34;
+
+ Assert.Equal(34, valueDictionary.GetOrAdd(12, x => 34));
+ }
+
+ [Fact]
+ public void GetOrAdd_should_return_default_and_add_it_if_key_not_exists()
+ {
+ Assert.Equal(24, valueDictionary.GetOrAdd(12, x => 24));
+ Assert.Equal(24, valueDictionary[12]);
+ }
+
+ [Fact]
+ public void GetOrNew_should_return_value_if_key_exists()
+ {
+ var list = new List();
+ listDictionary[12] = list;
+
+ Assert.Equal(list, listDictionary.GetOrNew(12));
+ }
+
+ [Fact]
+ public void GetOrNew_should_return_default_but_not_add_it_if_key_not_exists()
+ {
+ var list = new List();
+
+ Assert.Equal(list, listDictionary.GetOrNew(12));
+ Assert.False(listDictionary.ContainsKey(12));
+ }
+
+ [Fact]
+ public void GetOrAddNew_should_return_value_if_key_exists()
+ {
+ var list = new List();
+ listDictionary[12] = list;
+
+ Assert.Equal(list, listDictionary.GetOrAddNew(12));
+ }
+
+ [Fact]
+ public void GetOrAddNew_should_return_default_but_not_add_it_if_key_not_exists()
+ {
+ var list = new List();
+
+ Assert.Equal(list, listDictionary.GetOrAddNew(12));
+ Assert.Equal(list, listDictionary[12]);
+ }
+
+ [Fact]
+ public void SequentialHashCode_should_return_same_hash_codes_for_list_with_same_order()
+ {
+ var collection1 = new[] { 3, 5, 6 };
+ var collection2 = new[] { 3, 5, 6 };
+
+ Assert.Equal(collection2.SequentialHashCode(), collection1.SequentialHashCode());
+ }
+
+ [Fact]
+ public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_items()
+ {
+ var collection1 = new[] { 3, 5, 6 };
+ var collection2 = new[] { 3, 4, 1 };
+
+ Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode());
+ }
+
+ [Fact]
+ public void SequentialHashCode_should_return_different_hash_codes_for_list_with_different_order()
+ {
+ var collection1 = new[] { 3, 5, 6 };
+ var collection2 = new[] { 6, 5, 3 };
+
+ Assert.NotEqual(collection2.SequentialHashCode(), collection1.SequentialHashCode());
+ }
+
+ [Fact]
+ public void OrderedHashCode_should_return_same_hash_codes_for_list_with_same_order()
+ {
+ var collection1 = new[] { 3, 5, 6 };
+ var collection2 = new[] { 3, 5, 6 };
+
+ Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode());
+ }
+
+ [Fact]
+ public void OrderedHashCode_should_return_different_hash_codes_for_list_with_different_items()
+ {
+ var collection1 = new[] { 3, 5, 6 };
+ var collection2 = new[] { 3, 4, 1 };
+
+ Assert.NotEqual(collection2.OrderedHashCode(), collection1.OrderedHashCode());
+ }
+
+ [Fact]
+ public void OrderedHashCode_should_return_same_hash_codes_for_list_with_different_order()
+ {
+ var collection1 = new[] { 3, 5, 6 };
+ var collection2 = new[] { 6, 5, 3 };
+
+ Assert.Equal(collection2.OrderedHashCode(), collection1.OrderedHashCode());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/EnumExtensionsTest.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/EnumExtensionsTest.cs
new file mode 100644
index 000000000..17eb65aaa
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/EnumExtensionsTest.cs
@@ -0,0 +1,29 @@
+// ==========================================================================
+// EnumExtensionsTest.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using Xunit;
+
+namespace PinkParrot.Infrastructure
+{
+ public sealed class EnumExtensionsTest
+ {
+ [Fact]
+ public void Should_return_true_if_enum_is_valid()
+ {
+ Assert.True(DateTimeKind.Local.IsEnumValue());
+ }
+
+ [Fact]
+ public void Should_return_false_if_enum_is_not_valid()
+ {
+ Assert.False(((DateTimeKind)13).IsEnumValue());
+ Assert.False(123.IsEnumValue());
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/GuardTests.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/GuardTests.cs
new file mode 100644
index 000000000..ec4417a14
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/GuardTests.cs
@@ -0,0 +1,128 @@
+// ==========================================================================
+// GuardTests.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using Xunit;
+
+namespace PinkParrot.Infrastructure
+{
+ public class GuardTest
+ {
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ public void Should_throw_when_target_is_null_for_empty_string(string invalidString)
+ {
+ Assert.Throws(() => Guard.NotNullOrEmpty(invalidString, "parameter"));
+ }
+
+ [Fact]
+ public void Should_do_nothing_if_target_string_is_valid()
+ {
+ Guard.NotNullOrEmpty("value", "parameter");
+ }
+
+ [Fact]
+ public void Should_do_nothing_if_target_is_not_null()
+ {
+ Guard.NotNull("value", "parameter");
+ }
+
+ [Fact]
+ public void Should_do_nothing_if_enum_is_valid()
+ {
+ Guard.Enum(DateTimeKind.Local, "Parameter");
+ }
+
+ [Fact]
+ public void Should_throw_if_enum_is_not_valid()
+ {
+ Assert.Throws(() => Guard.Enum((DateTimeKind)13, "Parameter"));
+ }
+
+ [Fact]
+ public void Should_do_nothing_when_guid_is_not_empty()
+ {
+ Guard.NotEmpty(Guid.NewGuid(), "parameter");
+ }
+
+ [Fact]
+ public void Should_throw_when_guid_is_empty()
+ {
+ Assert.Throws(() => Guard.NotEmpty(Guid.Empty, "parameter"));
+ }
+
+ [Fact]
+ public void Should_throw_when_target_is_null()
+ {
+ Assert.Throws(() => Guard.NotNull(null, "parameter"));
+ }
+
+ [Fact]
+ public void Should_throw_when_target_is_null_for_null_string()
+ {
+ Assert.Throws(() => Guard.NotNullOrEmpty(null, "parameter"));
+ }
+
+ [Fact]
+ public void Should_do_nothing_when_target_has_correct_type()
+ {
+ Guard.HasType(123, "parameter");
+ }
+
+ [Fact]
+ public void Should_throw_when_target_has_wrong_type()
+ {
+ Assert.Throws(() => Guard.HasType("value", "parameter"));
+ }
+
+ [Fact]
+ public void Should_throw_when_checking_for_null_and_target_is_null()
+ {
+ Assert.Throws(() => Guard.HasType(null, "parameter"));
+ }
+
+ [Fact]
+ public void Should_do_nothing_when_target_is_not_default_value()
+ {
+ Guard.NotDefault(Guid.NewGuid(), "parameter");
+ }
+
+ [Fact]
+ public void Should_throw_exception_when_value_has_default()
+ {
+ Assert.Throws(() => Guard.NotDefault(Guid.Empty, "parameter"));
+ Assert.Throws(() => Guard.NotDefault(0, "parameter"));
+ Assert.Throws(() => Guard.NotDefault((string)null, "parameter"));
+ Assert.Throws(() => Guard.NotDefault(false, "parameter"));
+ }
+
+ [Theory]
+ [InlineData("")]
+ [InlineData(" ")]
+ [InlineData(" Not a Slug ")]
+ [InlineData(" not--a--slug ")]
+ [InlineData(" not-a-slug ")]
+ [InlineData("-not-a-slug-")]
+ [InlineData("not$-a-slug")]
+ public void Should_throw_exception_for_invalid_slug(string slug)
+ {
+ Assert.Throws(() => Guard.ValidSlug(slug, "slug"));
+ }
+
+ [Theory]
+ [InlineData("slug")]
+ [InlineData("slug23")]
+ [InlineData("other-slug")]
+ [InlineData("just-another-slug")]
+ public void Should_do_nothing_for_valid_slug(string slug)
+ {
+ Guard.ValidSlug(slug, "parameter");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PinkParrot.Infrastructure.Tests.xproj b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PinkParrot.Infrastructure.Tests.xproj
new file mode 100644
index 000000000..d1144a582
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PinkParrot.Infrastructure.Tests.xproj
@@ -0,0 +1,19 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 840c02b1-48f8-4c8a-8862-8a3fdefde8d5
+ PinkParrot.Infrastructure
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs
new file mode 100644
index 000000000..9c8b75362
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/PropertiesBagTests.cs
@@ -0,0 +1,281 @@
+// ==========================================================================
+// PropertiesBagTests.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Globalization;
+using System.Linq;
+using NodaTime;
+using Xunit;
+
+// ReSharper disable PossibleInvalidOperationException
+// ReSharper disable UnusedParameter.Local
+
+namespace PinkParrot.Infrastructure
+{
+ public class PropertiesBagTest
+ {
+ private readonly CultureInfo c = CultureInfo.InvariantCulture;
+ private readonly PropertiesBag bag = new PropertiesBag();
+
+ [Fact]
+ public void Should_return_false_when_renaming_unknown_property()
+ {
+ Assert.False(bag.Rename("OldKey", "NewKey"));
+ }
+
+ [Fact]
+ public void Should_throw_when_renaming_to_existing_property()
+ {
+ bag.Set("NewKey", 1);
+
+ Assert.Throws(() => bag.Rename("OldKey", "NewKey"));
+ }
+
+ [Fact]
+ public void Should_throw_when_renaming_to_same_key()
+ {
+ Assert.Throws(() => bag.Rename("SameKey", "SameKey"));
+ }
+
+ [Fact]
+ public void Should_provide_property_with_new_name_after_rename()
+ {
+ bag.Set("OldKey", 123);
+
+ Assert.True(bag.Rename("OldKey", "NewKey"));
+ Assert.True(bag.Contains("NewKey"));
+
+ Assert.Equal(1, bag.Count);
+ Assert.Equal(123, bag["NewKey"].ToInt32(c));
+
+ Assert.False(bag.Contains("OldKey"));
+ }
+
+ [Fact]
+ public void Should_calculate_count_correctly()
+ {
+ bag.Set("Key1", 1);
+ bag.Set("Key2", 1);
+
+ Assert.Equal(2, bag.Count);
+ Assert.Equal(new[] { "Key1", "Key2" }, bag.PropertyNames.ToArray());
+ Assert.Equal(new[] { "Key1", "Key2" }, bag.Properties.Keys.ToArray());
+ }
+
+ [Fact]
+ public void Should_return_correct_value_when_contains_check()
+ {
+ Assert.False(bag.Contains("Key"));
+
+ bag.Set("Key", 1);
+
+ Assert.True(bag.Contains("Key"));
+ Assert.True(bag.Contains("KEY"));
+ }
+
+ [Fact]
+ public void Should_returne_false_when_property_to_rename_does_not_exist()
+ {
+ Assert.False(bag.Remove("NOTFOUND"));
+ }
+
+ [Fact]
+ public void Should_ignore_casing_when_returning()
+ {
+ bag.Set("Key", 1);
+
+ Assert.True(bag.Remove("KEY"));
+ Assert.False(bag.Contains("KEY"));
+ }
+
+ [Fact]
+ public void Should_throw_when_setting_value_with_invalid_type()
+ {
+ Assert.Throws(() => bag.Set("Key", (byte)1));
+ }
+
+ [Fact]
+ public void Should_convert_string_to_numbers()
+ {
+ bag.Set("Key", 123);
+
+ AssertNumber();
+ }
+
+ [Fact]
+ public void Should_convert_int_to_numbers()
+ {
+ bag.Set("Key", 123);
+
+ AssertNumber();
+ }
+
+ [Fact]
+ public void Should_convert_long_to_numbers()
+ {
+ bag.Set("Key", 123L);
+
+ AssertNumber();
+ }
+
+ [Fact]
+ public void Should_throw_when_casting_from_large_long()
+ {
+ bag.Set("Key", long.MaxValue);
+
+ Assert.Throws(() => bag["Key"].ToInt32(c));
+ }
+
+ [Fact]
+ public void Should_convert_float_to_number()
+ {
+ bag.Set("Key", 123f);
+
+ AssertNumber();
+ }
+
+ [Fact]
+ public void Should_convert_double_to_number()
+ {
+ bag.Set("Key", 123d);
+
+ AssertNumber();
+ }
+
+ [Fact]
+ public void Should_throw_when_casting_from_large_doule()
+ {
+ bag.Set("Key", double.MaxValue);
+
+ Assert.Equal(float.PositiveInfinity, bag["Key"].ToSingle(c));
+ }
+
+ [Fact]
+ public void Should_convert_from_instant_value()
+ {
+ var time = SystemClock.Instance.GetCurrentInstant();
+
+ bag.Set("Key", time);
+
+ AssertInstant(time);
+ }
+
+ [Fact]
+ public void Should_convert_from_instant_string()
+ {
+ var time = SystemClock.Instance.GetCurrentInstant();
+
+ bag.Set("Key", time.ToString());
+
+ AssertInstant(time);
+ }
+
+ [Fact]
+ public void Should_convert_from_guid_value()
+ {
+ var id = new Guid();
+
+ bag.Set("Key", id);
+
+ AssertGuid(id);
+ }
+
+ [Fact]
+ public void Should_convert_from_guid_string()
+ {
+ var id = new Guid();
+
+ bag.Set("Key", id.ToString());
+
+ AssertGuid(id);
+ }
+
+ [Fact]
+ public void Should_convert_from_boolean_value()
+ {
+ bag.Set("Key", true);
+
+ AssertBoolean();
+ }
+
+ [Fact]
+ public void Should_convert_from_boolean_string()
+ {
+ bag.Set("Key", "true");
+
+ AssertBoolean();
+ }
+
+ [Fact]
+ public void Should_convert_boolean_from_number()
+ {
+ bag.Set("Key", 1);
+
+ AssertBoolean();
+ }
+
+ [Fact]
+ public void Should_throw_when_converting_instant_to_number()
+ {
+ bag.Set("Key", SystemClock.Instance.GetCurrentInstant());
+
+ Assert.Throws(() => bag["Key"].ToGuid(CultureInfo.InvariantCulture));
+ }
+
+ [Fact]
+ public void Should_return_default_when_property_value_is_null()
+ {
+ bag.Set("Key", null);
+
+ Assert.Equal(null, bag["Key"].ToString());
+
+ Assert.Equal(0f, bag["Key"].ToSingle(CultureInfo.CurrentCulture));
+ Assert.Equal(0d, bag["Key"].ToDouble(CultureInfo.CurrentCulture));
+ Assert.Equal(0L, bag["Key"].ToInt64(CultureInfo.CurrentCulture));
+ Assert.Equal(0, bag["Key"].ToInt32(CultureInfo.CurrentCulture));
+
+ Assert.Equal(false, bag["Key"].ToBoolean(CultureInfo.CurrentCulture));
+
+ Assert.Equal(new Guid(), bag["Key"].ToGuid(CultureInfo.CurrentCulture));
+
+ Assert.Equal(new Instant(), bag["Key"].ToInstant(CultureInfo.CurrentCulture));
+ }
+
+ private void AssertBoolean()
+ {
+ Assert.True(bag["Key"].ToBoolean(c));
+ Assert.True(bag["Key"].ToNullableBoolean(c));
+ }
+
+ private void AssertInstant(Instant expected)
+ {
+ Assert.Equal(expected.ToUnixTimeSeconds(), bag["Key"].ToInstant(c).ToUnixTimeSeconds());
+ Assert.Equal(expected.ToUnixTimeSeconds(), bag["Key"].ToNullableInstant(c).Value.ToUnixTimeSeconds());
+ }
+
+ private void AssertGuid(Guid expected)
+ {
+ Assert.Equal(expected, bag["Key"].ToGuid(c));
+ Assert.Equal(expected, bag["Key"].ToNullableGuid(c));
+ }
+
+ private void AssertNumber()
+ {
+ Assert.Equal(123, bag["Key"].ToInt32(c));
+ Assert.Equal(123, bag["Key"].ToNullableInt32(c));
+ Assert.Equal(123L, bag["Key"].ToInt64(c));
+ Assert.Equal(123L, bag["Key"].ToNullableInt64(c));
+ Assert.Equal(123f, bag["Key"].ToSingle(c));
+ Assert.Equal(123f, bag["Key"].ToNullableSingle(c));
+ Assert.Equal(123d, bag["Key"].ToDouble(c));
+ Assert.Equal(123d, bag["Key"].ToNullableDouble(c));
+
+ Assert.True(bag["Key"].ToBoolean(c));
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/project.json b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/project.json
new file mode 100644
index 000000000..baaaf4223
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/project.json
@@ -0,0 +1,29 @@
+{
+ "version": "1.0.0-*",
+ "testRunner": "xunit",
+ "dependencies": {
+ "dotnet-test-xunit": "2.2.0-preview2-build1029",
+ "PinkParrot.Infrastructure": "1.0.0-*",
+ "xunit": "2.2.0-beta2-build3300"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "type": "platform",
+ "version": "1.0.0"
+ }
+ }
+ }
+ },
+ "buildOptions": {
+ "copyToOutput": {
+ "include": [
+ "xunit.runner.json"
+ ]
+ }
+ },
+ "tooling": {
+ "defaultNamespace": "PinkParrot.Core.Tests"
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/xunit.runner.json b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/xunit.runner.json
new file mode 100644
index 000000000..6b3f1f87d
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure.Tests/xunit.runner.json
@@ -0,0 +1,5 @@
+{
+ "diagnosticMessages": false,
+ "methodDisplay": "classAndMethod",
+ "parallelizeTestCollections": true
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/CommonHeaders.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/CommonHeaders.cs
new file mode 100644
index 000000000..7df63ae08
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/CommonHeaders.cs
@@ -0,0 +1,19 @@
+// ==========================================================================
+// CommonHeaders.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public sealed class CommonHeaders
+ {
+ public const string AggregateId = "AggregateId";
+ public const string CommitId = "CommitId";
+ public const string Timestamp = "Timestamp";
+ public const string EventId = "EventId";
+ public const string EventType = "EventType";
+ public const string EventNumber = "EventNumber";
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/DomainObject.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/DomainObject.cs
new file mode 100644
index 000000000..ab4a02804
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/DomainObject.cs
@@ -0,0 +1,105 @@
+// ==========================================================================
+// DomainObject.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public abstract class DomainObject : IAggregate, IEquatable
+ {
+ private readonly List> uncomittedEvents = new List>();
+ private readonly Guid id;
+ private int version;
+
+ public int Version
+ {
+ get { return version; }
+ }
+
+ public Guid Id
+ {
+ get { return id; }
+ }
+
+ protected DomainObject(Guid id, int version)
+ {
+ Guard.NotEmpty(id, nameof(id));
+ Guard.GreaterEquals(version, 0, nameof(version));
+
+ this.id = id;
+
+ this.version = version;
+ }
+
+ protected void RaiseEvent(Envelope envelope, bool disableApply = false) where TEvent : class, IEvent
+ {
+ Guard.NotNull(envelope, nameof(envelope));
+
+ uncomittedEvents.Add(envelope.To());
+
+ if (!disableApply)
+ {
+ ApplyEvent(envelope.Payload);
+ }
+ }
+
+ protected void RaiseEvent(IEvent @event, bool disableApply = false)
+ {
+ Guard.NotNull(@event, nameof(@event));
+
+ uncomittedEvents.Add(EnvelopeFactory.ForEvent(@event, id));
+
+ if (!disableApply)
+ {
+ ApplyEvent(@event);
+ }
+ }
+
+ protected void Apply(object @event)
+ {
+ }
+
+ private void ApplyEvent(dynamic @event)
+ {
+ Apply(@event);
+ version++;
+ }
+
+ void IAggregate.ApplyEvent(IEvent @event)
+ {
+ Apply(@event as dynamic);
+ version++;
+ }
+
+ void IAggregate.ClearUncommittedEvents()
+ {
+ uncomittedEvents.Clear();
+ }
+
+ ICollection> IAggregate.GetUncomittedEvents()
+ {
+ return uncomittedEvents;
+ }
+
+ public override int GetHashCode()
+ {
+ return id.GetHashCode();
+ }
+
+ public override bool Equals(object obj)
+ {
+ return Equals(obj as IAggregate);
+ }
+
+ public bool Equals(IAggregate other)
+ {
+ return other != null && other.Id.Equals(id);
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Envelope.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Envelope.cs
new file mode 100644
index 000000000..d79df3091
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/Envelope.cs
@@ -0,0 +1,54 @@
+// ==========================================================================
+// Envelope.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public class Envelope where TPayload : class
+ {
+ private readonly EnvelopeHeaders headers;
+ private readonly TPayload payload;
+
+ public EnvelopeHeaders Headers
+ {
+ get
+ {
+ return headers;
+ }
+ }
+
+ public TPayload Payload
+ {
+ get
+ {
+ return payload;
+ }
+ }
+
+ public Envelope(TPayload payload)
+ {
+ Guard.NotNull(payload, nameof(payload));
+
+ this.payload = payload;
+
+ headers = new EnvelopeHeaders();
+ }
+
+ public Envelope(TPayload payload, EnvelopeHeaders headers)
+ {
+ Guard.NotNull(payload, nameof(payload));
+ Guard.NotNull(headers, nameof(headers));
+
+ this.payload = payload;
+ this.headers = headers;
+ }
+
+ public Envelope To() where TOther : class
+ {
+ return new Envelope(payload as TOther, headers.Clone());
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeExtensions.cs
new file mode 100644
index 000000000..5d384486b
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeExtensions.cs
@@ -0,0 +1,77 @@
+// ==========================================================================
+// EnvelopeExtensions.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Globalization;
+using NodaTime;
+
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public static class EnvelopeExtensions
+ {
+ public static int EventNumber(this Envelope envelope) where T : class
+ {
+ return envelope.Headers[CommonHeaders.EventNumber].ToInt32(CultureInfo.InvariantCulture);
+ }
+
+ public static Envelope SetEventNumber(this Envelope envelope, int value) where T : class
+ {
+ envelope.Headers.Set(CommonHeaders.EventNumber, value);
+
+ return envelope;
+ }
+
+ public static Guid CommitId(this Envelope envelope) where T : class
+ {
+ return envelope.Headers[CommonHeaders.CommitId].ToGuid(CultureInfo.InvariantCulture);
+ }
+
+ public static Envelope SetCommitId(this Envelope envelope, Guid value) where T : class
+ {
+ envelope.Headers.Set(CommonHeaders.CommitId, value);
+
+ return envelope;
+ }
+
+ public static Guid AggregateId(this Envelope envelope) where T : class
+ {
+ return envelope.Headers[CommonHeaders.AggregateId].ToGuid(CultureInfo.InvariantCulture);
+ }
+
+ public static Envelope SetAggregateId(this Envelope envelope, Guid value) where T : class
+ {
+ envelope.Headers.Set(CommonHeaders.AggregateId, value);
+
+ return envelope;
+ }
+
+ public static Guid EventId(this Envelope envelope) where T : class
+ {
+ return envelope.Headers[CommonHeaders.EventId].ToGuid(CultureInfo.InvariantCulture);
+ }
+
+ public static Envelope SetEventId(this Envelope envelope, Guid value) where T : class
+ {
+ envelope.Headers.Set(CommonHeaders.EventId, value);
+
+ return envelope;
+ }
+
+ public static Instant Timestamp(this Envelope envelope) where T : class
+ {
+ return envelope.Headers[CommonHeaders.Timestamp].ToInstant(CultureInfo.InvariantCulture);
+ }
+
+ public static Envelope SetTimestamp(this Envelope envelope, Instant value) where T : class
+ {
+ envelope.Headers.Set(CommonHeaders.Timestamp, value);
+
+ return envelope;
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeFactory.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeFactory.cs
new file mode 100644
index 000000000..fcfa1d203
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeFactory.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// EnvelopeFactory.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using NodaTime;
+
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public static class EnvelopeFactory
+ {
+ public static Envelope ForEvent(IEvent @event, Guid aggregateId)
+ {
+ return new Envelope(@event)
+ .SetAggregateId(aggregateId)
+ .SetEventId(aggregateId)
+ .SetTimestamp(SystemClock.Instance.GetCurrentInstant());
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs
new file mode 100644
index 000000000..fb0092932
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/EnvelopeHeaders.cs
@@ -0,0 +1,24 @@
+// ==========================================================================
+// EnvelopeHeaders.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public sealed class EnvelopeHeaders : PropertiesBag
+ {
+ public EnvelopeHeaders Clone()
+ {
+ var clone = new EnvelopeHeaders();
+
+ foreach (var property in Properties)
+ {
+ clone.Set(property.Key, property.Value.RawValue);
+ }
+
+ return clone;
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/IAggregate.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/IAggregate.cs
new file mode 100644
index 000000000..f640513c4
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/IAggregate.cs
@@ -0,0 +1,28 @@
+// ==========================================================================
+// IAggregate.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public interface IAggregate
+ {
+ Guid Id { get; }
+
+ int Version { get; }
+
+ void ApplyEvent(IEvent @event);
+
+ void ClearUncommittedEvents();
+
+ ICollection> GetUncomittedEvents();
+ }
+}
+
+
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/IEvent.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/IEvent.cs
new file mode 100644
index 000000000..6ee3da7ea
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CQRS/IEvent.cs
@@ -0,0 +1,13 @@
+// ==========================================================================
+// IEvent.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+namespace PinkParrot.Infrastructure.CQRS
+{
+ public interface IEvent
+ {
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs
new file mode 100644
index 000000000..dc47c0a3d
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/CollectionExtensions.cs
@@ -0,0 +1,110 @@
+// ==========================================================================
+// CollectionExtensions.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+// ReSharper disable InvertIf
+// ReSharper disable LoopCanBeConvertedToQuery
+
+namespace PinkParrot.Infrastructure
+{
+ public static class CollectionExtensions
+ {
+ public static int SequentialHashCode(this IEnumerable collection)
+ {
+ return collection.SequentialHashCode(EqualityComparer.Default);
+ }
+
+ public static int SequentialHashCode(this IEnumerable collection, IEqualityComparer comparer)
+ {
+ var hashCode = 17;
+
+ foreach (var item in collection)
+ {
+ if (item != null)
+ {
+ hashCode = hashCode * 23 + item.GetHashCode();
+ }
+ }
+
+ return hashCode;
+ }
+
+ public static int OrderedHashCode(this IEnumerable collection)
+ {
+ return collection.OrderedHashCode(EqualityComparer.Default);
+ }
+
+ public static int OrderedHashCode(this IEnumerable collection, IEqualityComparer comparer)
+ {
+ var hashCodes = collection.Where(x => x != null).Select(x => x.GetHashCode()).OrderBy(x => x).ToArray();
+
+ var hashCode = 17;
+
+ foreach (var code in hashCodes)
+ {
+ hashCode = hashCode * 23 + code;
+ }
+
+ return hashCode;
+ }
+
+ public static bool EqualsDictionary(this IReadOnlyDictionary dictionary, IDictionary other)
+ {
+ return Equals(dictionary, other) || (other != null && dictionary.Count == other.Count && !dictionary.Except(other).Any());
+ }
+
+ public static TValue GetOrDefault(this IReadOnlyDictionary dictionary, TKey key)
+ {
+ return dictionary.GetOrCreate(key, _ => default(TValue));
+ }
+
+ public static TValue GetOrAddDefault(this IDictionary dictionary, TKey key)
+ {
+ return dictionary.GetOrAdd(key, _ => default(TValue));
+ }
+
+ public static TValue GetOrNew(this IReadOnlyDictionary dictionary, TKey key) where TValue : class, new()
+ {
+ return dictionary.GetOrCreate(key, _ => new TValue());
+ }
+
+ public static TValue GetOrAddNew(this IDictionary dictionary, TKey key) where TValue : class, new()
+ {
+ return dictionary.GetOrAdd(key, _ => new TValue());
+ }
+
+ public static TValue GetOrCreate(this IReadOnlyDictionary dictionary, TKey key, Func creator)
+ {
+ TValue result;
+
+ if (!dictionary.TryGetValue(key, out result))
+ {
+ result = creator(key);
+ }
+
+ return result;
+ }
+
+ public static TValue GetOrAdd(this IDictionary dictionary, TKey key, Func creator)
+ {
+ TValue result;
+
+ if (!dictionary.TryGetValue(key, out result))
+ {
+ result = creator(key);
+
+ dictionary.Add(key, result);
+ }
+
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/DomainValidationException.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/DomainValidationException.cs
new file mode 100644
index 000000000..5b3ea411a
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/DomainValidationException.cs
@@ -0,0 +1,48 @@
+// ==========================================================================
+// DomainValidationException.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace PinkParrot.Infrastructure
+{
+ public class DomainValidationException : Exception
+ {
+ private readonly IReadOnlyList errors;
+
+ public IReadOnlyList Errors
+ {
+ get { return errors; }
+ }
+
+ public DomainValidationException(string message, params string[] errors)
+ : base(message)
+ {
+ this.errors = errors != null ? errors.ToList() : new List();
+ }
+
+ public DomainValidationException(string message, IReadOnlyList errors)
+ : base(message)
+ {
+ this.errors = errors ?? new List();
+ }
+
+ public DomainValidationException(string message, Exception inner, params string[] errors)
+ : base(message, inner)
+ {
+ this.errors = errors != null ? errors.ToList() : new List();
+ }
+
+ public DomainValidationException(string message, Exception inner, IReadOnlyList errors)
+ : base(message, inner)
+ {
+ this.errors = errors ?? new List();
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/EnumExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/EnumExtensions.cs
new file mode 100644
index 000000000..4b01cb588
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/EnumExtensions.cs
@@ -0,0 +1,27 @@
+// ==========================================================================
+// EnumExtensions.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+
+namespace PinkParrot.Infrastructure
+{
+ public static class EnumExtensions
+ {
+ public static bool IsEnumValue(this TEnum value) where TEnum : struct
+ {
+ try
+ {
+ return Enum.IsDefined(typeof(TEnum), value);
+ }
+ catch
+ {
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Extensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Extensions.cs
new file mode 100644
index 000000000..1d2b453cd
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Extensions.cs
@@ -0,0 +1,29 @@
+// ==========================================================================
+// Extensions.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace PinkParrot.Infrastructure
+{
+ public static class Extensions
+ {
+ private static readonly Regex SlugRegex = new Regex("^[a-z0-9]+(\\-[a-z0-9]+)*$", RegexOptions.Compiled);
+
+ public static bool IsSlug(this string value)
+ {
+ return value != null && SlugRegex.IsMatch(value);
+ }
+
+ public static bool IsBetween(this TValue value, TValue low, TValue high) where TValue : IComparable
+ {
+ return Comparer.Default.Compare(low, value) <= 0 && Comparer.Default.Compare(high, value) >= 0;
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Guard.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Guard.cs
new file mode 100644
index 000000000..f999e5560
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Guard.cs
@@ -0,0 +1,240 @@
+// ==========================================================================
+// Guard.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Runtime.CompilerServices;
+
+// ReSharper disable InvertIf
+
+namespace PinkParrot.Infrastructure
+{
+ public static class Guard
+ {
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ValidNumber(float target, string parameterName)
+ {
+ if (float.IsNaN(target) || float.IsPositiveInfinity(target) || float.IsNegativeInfinity(target))
+ {
+ throw new ArgumentException("Value must be a valid number.", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ValidNumber(double target, string parameterName)
+ {
+ if (double.IsNaN(target) || double.IsPositiveInfinity(target) || double.IsNegativeInfinity(target))
+ {
+ throw new ArgumentException("Value must be a valid number.", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ValidSlug(string target, string parameterName)
+ {
+ NotNullOrEmpty(target, parameterName);
+
+ if (!target.IsSlug())
+ {
+ throw new ArgumentException("Target is not a valid slug.", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void HasType(object target, string parameterName)
+ {
+ NotNull(target, "parameterName");
+
+ if (target.GetType() != typeof(T))
+ {
+ throw new ArgumentException("The parameter must be of type " + typeof(T), parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Between(TValue target, TValue lower, TValue upper, string parameterName) where TValue : IComparable
+ {
+ if (!target.IsBetween(lower, upper))
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be between {0} and {1}", lower, upper);
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void Enum(TEnum target, string parameterName) where TEnum : struct
+ {
+ if (!target.IsEnumValue())
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be a valid enum type {0}", typeof(TEnum));
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void GreaterThan(TValue target, TValue lower, string parameterName) where TValue : IComparable
+ {
+ if (target.CompareTo(lower) <= 0)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be greater than {0}", lower);
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void GreaterEquals(TValue target, TValue lower, string parameterName) where TValue : IComparable
+ {
+ if (target.CompareTo(lower) < 0)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be greater than {0}", lower);
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LessThan(TValue target, TValue upper, string parameterName) where TValue : IComparable
+ {
+ if (target.CompareTo(upper) >= 0)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be less than {0}", upper);
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void LessEquals(TValue target, TValue upper, string parameterName) where TValue : IComparable
+ {
+ if (target.CompareTo(upper) > 0)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be less than {0}", upper);
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void NotEmpty(ICollection enumerable, string parameterName)
+ {
+ if (enumerable == null)
+ {
+ throw new ArgumentNullException(nameof(enumerable));
+ }
+
+ if (enumerable.Count == 0)
+ {
+ throw new ArgumentException("Collection does not contain an item", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void NotEmpty(Guid target, string parameterName)
+ {
+ if (target == Guid.Empty)
+ {
+ throw new ArgumentException("Value cannot be empty.", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void NotNull(object target, string parameterName)
+ {
+ if (target == null)
+ {
+ throw new ArgumentNullException(parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void NotDefault(T target, string parameterName)
+ {
+ if (Equals(target, default(T)))
+ {
+ throw new ArgumentException("Value cannot be an the default value", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void NotNullOrEmpty(string target, string parameterName, bool allowWhitespacesAtStartOrEnd = true)
+ {
+ if (target == null)
+ {
+ throw new ArgumentNullException(parameterName);
+ }
+
+ if (string.IsNullOrWhiteSpace(target))
+ {
+ throw new ArgumentException("String parameter cannot be null or empty and cannot contain only blanks.", parameterName);
+ }
+
+ if (!allowWhitespacesAtStartOrEnd && target.Trim() != target)
+ {
+ throw new ArgumentException("String cannot start or end with whitespaces", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void ValidFileName(string target, string parameterName)
+ {
+ NotNullOrEmpty(target, parameterName);
+
+ if (target.Intersect(Path.GetInvalidFileNameChars()).Any())
+ {
+ throw new ArgumentException("Value contains an invalid character.", parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void IsType(object target, string parameterName)
+ {
+ if (target != null && target.GetType() != typeof(T))
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be of type {0}", typeof(T));
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+
+ [DebuggerStepThrough]
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void IsType(object target, Type expectedType, string parameterName)
+ {
+ if (target != null && expectedType != null && target.GetType() != expectedType)
+ {
+ var message = string.Format(CultureInfo.CurrentCulture, "Value must be of type {0}", expectedType);
+
+ throw new ArgumentException(message, parameterName);
+ }
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PinkParrot.Infrastructure.xproj b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PinkParrot.Infrastructure.xproj
new file mode 100644
index 000000000..c7fc160b4
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PinkParrot.Infrastructure.xproj
@@ -0,0 +1,21 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+ bd1c30a8-8ffa-4a92-a9bd-b67b1cddd84c
+ PinkParrot.Infrastructure
+ .\obj
+ .\bin\
+ v4.5.2
+
+
+
+ 2.0
+
+
+
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs
new file mode 100644
index 000000000..4d773e167
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertiesBag.cs
@@ -0,0 +1,94 @@
+// ==========================================================================
+// PropertiesBag.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+
+namespace PinkParrot.Infrastructure
+{
+ public class PropertiesBag
+ {
+ private readonly Dictionary internalDictionary = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ public int Count
+ {
+ get { return internalDictionary.Count; }
+ }
+
+ public IReadOnlyDictionary Properties
+ {
+ get { return internalDictionary; }
+ }
+
+ public IEnumerable PropertyNames
+ {
+ get { return internalDictionary.Keys; }
+ }
+
+ public PropertyValue this[string propertyName]
+ {
+ get
+ {
+ Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
+
+ return internalDictionary[propertyName];
+ }
+ }
+
+ public bool Contains(string propertyName)
+ {
+ Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
+
+ return internalDictionary.ContainsKey(propertyName);
+ }
+
+ public PropertiesBag Set(string propertyName, object value)
+ {
+ Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
+
+ internalDictionary[propertyName] = new PropertyValue(value);
+
+ return this;
+ }
+
+ public bool Remove(string propertyName)
+ {
+ Guard.NotNullOrEmpty(propertyName, nameof(propertyName));
+
+ return internalDictionary.Remove(propertyName);
+ }
+
+ public bool Rename(string oldPropertyName, string newPropertyName)
+ {
+ Guard.NotNullOrEmpty(oldPropertyName, nameof(oldPropertyName));
+ Guard.NotNullOrEmpty(newPropertyName, nameof(newPropertyName));
+
+ if (internalDictionary.ContainsKey(newPropertyName))
+ {
+ throw new ArgumentException($"An property with the key '{newPropertyName}' already exists.", newPropertyName);
+ }
+
+ if (string.Equals(oldPropertyName, newPropertyName, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new ArgumentException($"The property names '{newPropertyName}' are equal.", newPropertyName);
+ }
+
+ PropertyValue property;
+
+ if (!internalDictionary.TryGetValue(oldPropertyName, out property))
+ {
+ return false;
+ }
+
+ internalDictionary[newPropertyName] = property;
+ internalDictionary.Remove(oldPropertyName);
+
+ return true;
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs
new file mode 100644
index 000000000..404ad969e
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/PropertyValue.cs
@@ -0,0 +1,211 @@
+// ==========================================================================
+// PropertyValue.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using NodaTime;
+using NodaTime.Text;
+
+namespace PinkParrot.Infrastructure
+{
+ public sealed class PropertyValue
+ {
+ private readonly object rawValue;
+
+ private static readonly HashSet AllowedTypes = new HashSet
+ {
+ typeof(string),
+ typeof(bool),
+ typeof(bool?),
+ typeof(float),
+ typeof(float?),
+ typeof(double),
+ typeof(double?),
+ typeof(int),
+ typeof(int?),
+ typeof(long),
+ typeof(long?),
+ typeof(Instant),
+ typeof(Instant?),
+ typeof(Guid),
+ typeof(Guid?)
+ };
+
+ public object RawValue
+ {
+ get { return rawValue; }
+ }
+
+ internal PropertyValue(object rawValue)
+ {
+ if (rawValue != null && !AllowedTypes.Contains(rawValue.GetType()))
+ {
+ throw new ArgumentException("The type is not supported.", nameof(rawValue));
+ }
+
+ this.rawValue = rawValue;
+ }
+
+ public override string ToString()
+ {
+ return rawValue?.ToString();
+ }
+
+ public bool ToBoolean(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, bool.Parse);
+ }
+
+ public bool? ToNullableBoolean(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, bool.Parse);
+ }
+
+ public float ToSingle(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, x => float.Parse(x, culture));
+ }
+
+ public float? ToNullableSingle(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, x => float.Parse(x, culture));
+ }
+
+ public double ToDouble(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, x => double.Parse(x, culture));
+ }
+
+ public double? ToNullableDouble(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, x => double.Parse(x, culture));
+ }
+
+ public int ToInt32(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, x => int.Parse(x, culture));
+ }
+
+ public int? ToNullableInt32(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, x => int.Parse(x, culture));
+ }
+
+ public long ToInt64(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, x => long.Parse(x, culture));
+ }
+
+ public long? ToNullableInt64(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, x => long.Parse(x, culture));
+ }
+
+ public Instant ToInstant(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, x => InstantPattern.GeneralPattern.Parse(x).Value);
+ }
+
+ public Instant? ToNullableInstant(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, x => InstantPattern.GeneralPattern.Parse(x).Value);
+ }
+
+ public Guid ToGuid(CultureInfo culture)
+ {
+ return ToOrParseValue(culture, Guid.Parse);
+ }
+
+ public Guid? ToNullableGuid(CultureInfo culture)
+ {
+ return ToNullableOrParseValue(culture, Guid.Parse);
+ }
+
+ private T? ToNullableOrParseValue(IFormatProvider culture, Func parser) where T : struct
+ {
+ T result;
+
+ return TryParse(culture, parser, out result) ? result : (T?)null;
+ }
+
+ private T ToOrParseValue(IFormatProvider culture, Func parser)
+ {
+ T result;
+
+ return TryParse(culture, parser, out result) ? result : default(T);
+ }
+
+ private bool TryParse(IFormatProvider culture, Func parser, out T result)
+ {
+ var value = rawValue;
+
+ if (value != null)
+ {
+ var valueType = value.GetType();
+
+ if (valueType == typeof(T))
+ {
+ result = (T)value;
+ }
+ else if (valueType == typeof(string))
+ {
+ result = Parse(parser, valueType, value);
+ }
+ else
+ {
+ result = Convert(culture, value, valueType);
+ }
+
+ return true;
+ }
+
+ result = default(T);
+
+ return false;
+ }
+
+ private static T Convert(IFormatProvider culture, object value, Type valueType)
+ {
+ var requestedType = typeof(T);
+
+ try
+ {
+ return (T)System.Convert.ChangeType(value, requestedType, culture);
+ }
+ catch (OverflowException)
+ {
+ string message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}' because it is either too small or large.";
+
+ throw new InvalidCastException(message);
+ }
+ catch (InvalidCastException)
+ {
+ string message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}'.";
+
+ throw new InvalidCastException(message);
+ }
+ }
+
+ private static T Parse(Func parser, Type valueType, object value)
+ {
+ var requestedType = typeof(T);
+
+ try
+ {
+ return parser(value.ToString());
+ }
+ catch (Exception e)
+ {
+ string message = $"The property has type '{valueType}' and cannot be casted to '{requestedType}'.";
+
+ throw new InvalidCastException(message, e);
+ }
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Tasks/TaskExtensions.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Tasks/TaskExtensions.cs
new file mode 100644
index 000000000..ad2eee47c
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Tasks/TaskExtensions.cs
@@ -0,0 +1,19 @@
+// ==========================================================================
+// TaskExtensions.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System.Threading.Tasks;
+
+namespace PinkParrot.Infrastructure.Tasks
+{
+ public static class TaskExtensions
+ {
+ public static void Forget(this Task task)
+ {
+ }
+ }
+}
diff --git a/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Tasks/TaskHelper.cs b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Tasks/TaskHelper.cs
new file mode 100644
index 000000000..f7848ce9b
--- /dev/null
+++ b/src/pinkparrot_infrastructure/PinkParrot.Infrastructure/Tasks/TaskHelper.cs
@@ -0,0 +1,26 @@
+// ==========================================================================
+// TaskHelper.cs
+// PinkParrot Headless CMS
+// ==========================================================================
+// Copyright (c) PinkParrot Group
+// All rights reserved.
+// ==========================================================================
+
+using System.Threading.Tasks;
+
+namespace PinkParrot.Infrastructure.Tasks
+{
+ public static class TaskHelper
+ {
+ public static readonly Task Done = CreateDoneTask();
+
+ private static Task CreateDoneTask()
+ {
+ var result = new TaskCompletionSource