From ec4482b20d54af9295379be5cc536e899da90972 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Sun, 1 Nov 2015 17:25:55 -0800 Subject: [PATCH 0001/1585] Create README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 000000000000..96af369095c2 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +NodeServices +======== + +This repo hosts the Microsoft.AspNet.NodeServices project. + +This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. From d77a8aa7a2151031f3322b3c6833217199ea9151 Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Sun, 1 Nov 2015 17:26:21 -0800 Subject: [PATCH 0002/1585] Create CONTRIBUTING.md --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..64ff041d5caf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Contributing +====== + +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. From 0e1fa2e09d21c1e5390f637f681cc59c977de42a Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Sun, 1 Nov 2015 17:26:35 -0800 Subject: [PATCH 0003/1585] Create LICENSE.txt --- LICENSE.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 LICENSE.txt diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000000..0bdc1962b610 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. From f693bd60e36f84228b2d5ade2fcb3ab685c7bc27 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 10:30:36 -0800 Subject: [PATCH 0004/1585] Initial state --- .gitignore | 3 + .../.gitignore | 1 + .../AngularPrerenderTagHelper.cs | 50 + .../Content/Node/angular-rendering.js | 28 + .../project.json | 34 + .../.gitignore | 1 + .../Content/Node/react-rendering.js | 40 + .../ReactRenderer.cs | 24 + .../project.json | 33 + Microsoft.AspNet.NodeServices/.gitignore | 1 + .../Content/Node/entrypoint-http.js | 62 + .../Content/Node/entrypoint-stream.js | 23 + .../HostingModels/EmbeddedResourceReader.cs | 17 + .../HostingModels/HttpNodeHost.cs | 50 + .../InputOutputStreamNodeHost.cs | 57 + .../HostingModels/NodeHost.cs | 10 + .../HostingModels/NodeInvocationInfo.cs | 8 + .../HostingModels/OutOfProcessNodeRunner.cs | 133 + .../NodeHostingModel.cs | 6 + Microsoft.AspNet.NodeServices/NodeInstance.cs | 38 + .../StringAsTempFile.cs | 39 + Microsoft.AspNet.NodeServices/project.json | 34 + samples/angular/MusicStore/.gitignore | 5 + .../MusicStore/Apis/AlbumsApiController.cs | 210 ++ .../MusicStore/Apis/ArtistsApiController.cs | 30 + .../MusicStore/Apis/GenresApiController.cs | 70 + .../Apis/Models/AccountViewModels.cs | 63 + .../angular/MusicStore/Apis/Models/Album.cs | 40 + .../angular/MusicStore/Apis/Models/Artist.cs | 12 + .../MusicStore/Apis/Models/CartItem.cs | 21 + .../angular/MusicStore/Apis/Models/Genre.cs | 24 + .../Apis/Models/MusicStoreContext.cs | 39 + .../angular/MusicStore/Apis/Models/Order.cs | 73 + .../MusicStore/Apis/Models/OrderDetail.cs | 14 + .../MusicStore/Apis/Models/SampleData.cs | 944 +++++++ .../MusicStore/Apis/Models/ShoppingCart.cs | 207 ++ .../MusicStore/Controllers/HomeController.cs | 27 + .../MusicStore/Infrastructure/ApiResult.cs | 63 + .../Infrastructure/NoCacheAttribute.cs | 19 + .../MusicStore/Infrastructure/PagedList.cs | 150 + .../Infrastructure/SortDirection.cs | 13 + .../Infrastructure/SortExpression.cs | 87 + samples/angular/MusicStore/SiteSettings.cs | 13 + samples/angular/MusicStore/Startup.cs | 139 + .../MusicStore/Views/Home/Index.cshtml | 21 + .../MusicStore/Views/Shared/Error.cshtml | 6 + .../MusicStore/Views/Shared/_Layout.cshtml | 40 + .../MusicStore/Views/_ViewImports.cshtml | 3 + .../MusicStore/Views/_ViewStart.cshtml | 3 + samples/angular/MusicStore/appsettings.json | 3 + samples/angular/MusicStore/gulpfile.js | 53 + samples/angular/MusicStore/package.json | 27 + samples/angular/MusicStore/project.json | 49 + samples/angular/MusicStore/tsconfig.json | 13 + .../angular/MusicStore/wwwroot/css/site.css | 3 + .../MusicStore/wwwroot/css/styles.less | 14 + .../wwwroot/images/home-showcase.png | Bin 0 -> 254130 bytes .../MusicStore/wwwroot/images/logo.png | Bin 0 -> 2963 bytes .../MusicStore/wwwroot/images/placeholder.png | Bin 0 -> 1221 bytes .../admin/admin-home/admin-home.html | 3 + .../components/admin/admin-home/admin-home.ts | 21 + .../album-delete-prompt.html | 17 + .../album-delete-prompt.ts | 25 + .../admin/album-details/album-details.html | 50 + .../admin/album-details/album-details.ts | 22 + .../admin/album-edit/album-edit.html | 45 + .../components/admin/album-edit/album-edit.ts | 82 + .../admin/albums-list/albums-list.html | 44 + .../admin/albums-list/albums-list.ts | 70 + .../admin/form-field/form-field.html | 9 + .../components/admin/form-field/form-field.ts | 20 + .../wwwroot/ng-app/components/app/app.css | 0 .../wwwroot/ng-app/components/app/app.html | 30 + .../wwwroot/ng-app/components/app/app.ts | 35 + .../ng-app/components/app/bootstrap.ts | 6 + .../public/album-details/album-details.html | 26 + .../public/album-details/album-details.ts | 21 + .../public/album-tile/album-tile.html | 4 + .../public/album-tile/album-tile.ts | 14 + .../public/genre-contents/genre-contents.html | 7 + .../public/genre-contents/genre-contents.ts | 22 + .../public/genres-list/genres-list.html | 13 + .../public/genres-list/genres-list.ts | 21 + .../ng-app/components/public/home/home.html | 10 + .../ng-app/components/public/home/home.ts | 21 + .../wwwroot/ng-app/models/models.ts | 16 + .../MusicStore/wwwroot/system.config.js | 3 + samples/angular/MusicStore/wwwroot/web.config | 9 + samples/react/ReactGrid/.gitignore | 5 + .../ReactGrid/Controllers/HomeController.cs | 24 + samples/react/ReactGrid/README.txt | 2 + .../react/ReactGrid/ReactApp/boot-client.jsx | 8 + .../ReactApp/components/CustomPager.jsx | 50 + .../ReactApp/components/PeopleGrid.jsx | 26 + .../ReactApp/components/ReactApp.jsx | 14 + .../ReactGrid/ReactApp/data/columnMeta.js | 47 + .../react/ReactGrid/ReactApp/data/fakeData.js | 2489 +++++++++++++++++ samples/react/ReactGrid/Startup.cs | 68 + .../react/ReactGrid/Views/Home/Index.cshtml | 5 + .../react/ReactGrid/Views/Shared/Error.cshtml | 6 + .../ReactGrid/Views/Shared/_Layout.cshtml | 12 + .../react/ReactGrid/Views/_ViewImports.cshtml | 2 + .../react/ReactGrid/Views/_ViewStart.cshtml | 3 + samples/react/ReactGrid/appsettings.json | 1 + samples/react/ReactGrid/jsconfig.json | 6 + samples/react/ReactGrid/package.json | 25 + samples/react/ReactGrid/project.json | 45 + samples/react/ReactGrid/webpack.config.js | 19 + samples/react/ReactGrid/wwwroot/favicon.ico | Bin 0 -> 32038 bytes samples/react/ReactGrid/wwwroot/web.config | 9 + 110 files changed, 6722 insertions(+) create mode 100644 .gitignore create mode 100644 Microsoft.AspNet.NodeServices.Angular/.gitignore create mode 100644 Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs create mode 100644 Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js create mode 100644 Microsoft.AspNet.NodeServices.Angular/project.json create mode 100644 Microsoft.AspNet.NodeServices.React/.gitignore create mode 100644 Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js create mode 100644 Microsoft.AspNet.NodeServices.React/ReactRenderer.cs create mode 100644 Microsoft.AspNet.NodeServices.React/project.json create mode 100644 Microsoft.AspNet.NodeServices/.gitignore create mode 100644 Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js create mode 100644 Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js create mode 100644 Microsoft.AspNet.NodeServices/HostingModels/EmbeddedResourceReader.cs create mode 100644 Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs create mode 100644 Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs create mode 100644 Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs create mode 100644 Microsoft.AspNet.NodeServices/HostingModels/NodeInvocationInfo.cs create mode 100644 Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs create mode 100644 Microsoft.AspNet.NodeServices/NodeHostingModel.cs create mode 100644 Microsoft.AspNet.NodeServices/NodeInstance.cs create mode 100644 Microsoft.AspNet.NodeServices/StringAsTempFile.cs create mode 100644 Microsoft.AspNet.NodeServices/project.json create mode 100644 samples/angular/MusicStore/.gitignore create mode 100644 samples/angular/MusicStore/Apis/AlbumsApiController.cs create mode 100644 samples/angular/MusicStore/Apis/ArtistsApiController.cs create mode 100644 samples/angular/MusicStore/Apis/GenresApiController.cs create mode 100644 samples/angular/MusicStore/Apis/Models/AccountViewModels.cs create mode 100644 samples/angular/MusicStore/Apis/Models/Album.cs create mode 100644 samples/angular/MusicStore/Apis/Models/Artist.cs create mode 100644 samples/angular/MusicStore/Apis/Models/CartItem.cs create mode 100644 samples/angular/MusicStore/Apis/Models/Genre.cs create mode 100644 samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs create mode 100644 samples/angular/MusicStore/Apis/Models/Order.cs create mode 100644 samples/angular/MusicStore/Apis/Models/OrderDetail.cs create mode 100644 samples/angular/MusicStore/Apis/Models/SampleData.cs create mode 100644 samples/angular/MusicStore/Apis/Models/ShoppingCart.cs create mode 100755 samples/angular/MusicStore/Controllers/HomeController.cs create mode 100644 samples/angular/MusicStore/Infrastructure/ApiResult.cs create mode 100644 samples/angular/MusicStore/Infrastructure/NoCacheAttribute.cs create mode 100644 samples/angular/MusicStore/Infrastructure/PagedList.cs create mode 100644 samples/angular/MusicStore/Infrastructure/SortDirection.cs create mode 100644 samples/angular/MusicStore/Infrastructure/SortExpression.cs create mode 100644 samples/angular/MusicStore/SiteSettings.cs create mode 100755 samples/angular/MusicStore/Startup.cs create mode 100755 samples/angular/MusicStore/Views/Home/Index.cshtml create mode 100755 samples/angular/MusicStore/Views/Shared/Error.cshtml create mode 100755 samples/angular/MusicStore/Views/Shared/_Layout.cshtml create mode 100755 samples/angular/MusicStore/Views/_ViewImports.cshtml create mode 100755 samples/angular/MusicStore/Views/_ViewStart.cshtml create mode 100755 samples/angular/MusicStore/appsettings.json create mode 100755 samples/angular/MusicStore/gulpfile.js create mode 100644 samples/angular/MusicStore/package.json create mode 100755 samples/angular/MusicStore/project.json create mode 100644 samples/angular/MusicStore/tsconfig.json create mode 100644 samples/angular/MusicStore/wwwroot/css/site.css create mode 100644 samples/angular/MusicStore/wwwroot/css/styles.less create mode 100644 samples/angular/MusicStore/wwwroot/images/home-showcase.png create mode 100644 samples/angular/MusicStore/wwwroot/images/logo.png create mode 100644 samples/angular/MusicStore/wwwroot/images/placeholder.png create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/app/app.css create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/app/app.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/app/app.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.html create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.ts create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/models/models.ts create mode 100644 samples/angular/MusicStore/wwwroot/system.config.js create mode 100644 samples/angular/MusicStore/wwwroot/web.config create mode 100644 samples/react/ReactGrid/.gitignore create mode 100755 samples/react/ReactGrid/Controllers/HomeController.cs create mode 100644 samples/react/ReactGrid/README.txt create mode 100644 samples/react/ReactGrid/ReactApp/boot-client.jsx create mode 100644 samples/react/ReactGrid/ReactApp/components/CustomPager.jsx create mode 100644 samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx create mode 100644 samples/react/ReactGrid/ReactApp/components/ReactApp.jsx create mode 100644 samples/react/ReactGrid/ReactApp/data/columnMeta.js create mode 100644 samples/react/ReactGrid/ReactApp/data/fakeData.js create mode 100755 samples/react/ReactGrid/Startup.cs create mode 100755 samples/react/ReactGrid/Views/Home/Index.cshtml create mode 100755 samples/react/ReactGrid/Views/Shared/Error.cshtml create mode 100755 samples/react/ReactGrid/Views/Shared/_Layout.cshtml create mode 100755 samples/react/ReactGrid/Views/_ViewImports.cshtml create mode 100755 samples/react/ReactGrid/Views/_ViewStart.cshtml create mode 100755 samples/react/ReactGrid/appsettings.json create mode 100644 samples/react/ReactGrid/jsconfig.json create mode 100644 samples/react/ReactGrid/package.json create mode 100755 samples/react/ReactGrid/project.json create mode 100644 samples/react/ReactGrid/webpack.config.js create mode 100755 samples/react/ReactGrid/wwwroot/favicon.ico create mode 100644 samples/react/ReactGrid/wwwroot/web.config diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000000..4fc10d27b394 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vs +*.xproj.user +project.lock.json diff --git a/Microsoft.AspNet.NodeServices.Angular/.gitignore b/Microsoft.AspNet.NodeServices.Angular/.gitignore new file mode 100644 index 000000000000..ae3c1726048c --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs new file mode 100644 index 000000000000..9a5292b80a98 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs @@ -0,0 +1,50 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.NodeServices; +using Microsoft.AspNet.Http.Extensions; + +namespace Microsoft.AspNet.NodeServices.Angular +{ + [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] + public class AngularRunAtServerTagHelper : TagHelper + { + static StringAsTempFile nodeScript; + + static AngularRunAtServerTagHelper() { + // Consider populating this lazily + var script = EmbeddedResourceReader.Read(typeof (AngularRunAtServerTagHelper), "/Content/Node/angular-rendering.js"); + nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + } + + const string PrerenderModuleAttributeName = "aspnet-ng2-prerender-module"; + const string PrerenderExportAttributeName = "aspnet-ng2-prerender-export"; + + private static NodeInstance nodeInstance = new NodeInstance(); + + [HtmlAttributeName(PrerenderModuleAttributeName)] + public string ModuleName { get; set; } + + [HtmlAttributeName(PrerenderExportAttributeName)] + public string ExportName { get; set; } + + private IHttpContextAccessor contextAccessor; + + public AngularRunAtServerTagHelper(IHttpContextAccessor contextAccessor) + { + this.contextAccessor = contextAccessor; + } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var result = await nodeInstance.InvokeExport(nodeScript.FileName, "renderComponent", new { + componentModule = this.ModuleName, + componentExport = this.ExportName, + tagName = output.TagName, + baseUrl = UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request) + }); + output.SuppressOutput(); + output.PostElement.AppendEncoded(result); + } + } +} diff --git a/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js b/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js new file mode 100644 index 000000000000..57b7df836980 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js @@ -0,0 +1,28 @@ +var path = require('path'); +var ngUniversal = require('angular2-universal-patched'); +var ng = require('angular2/angular2'); +var ngRouter = require('angular2/router'); + +module.exports = { + renderComponent: function(callback, options) { + // Find the component class. Use options.componentExport if specified, otherwise convert tag-name to PascalCase. + var loadedModule = require(path.resolve(process.cwd(), options.componentModule)); + var componentExport = options.componentExport || options.tagName.replace(/(-|^)([a-z])/g, function (m1, m2, char) { return char.toUpperCase(); }); + var component = loadedModule[componentExport]; + if (!component) { + throw new Error('The module "' + options.componentModule + '" has no export named "' + componentExport + '"'); + } + + var serverBindings = [ + ngRouter.ROUTER_BINDINGS, + ngUniversal.HTTP_PROVIDERS, + ng.provide(ngUniversal.BASE_URL, { useValue: options.baseUrl }), + ngUniversal.SERVER_LOCATION_PROVIDERS + ]; + + return ngUniversal.renderToString(component, serverBindings).then( + function(successValue) { callback(null, successValue); }, + function(errorValue) { callback(errorValue); } + ); + } +}; diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json new file mode 100644 index 000000000000..133451b8d406 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -0,0 +1,34 @@ +{ + "version": "1.0.0-alpha1", + "description": "Microsoft.AspNet.NodeServices.Angular Class Library", + "authors": [ + "Microsoft" + ], + "tags": [ + "" + ], + "projectUrl": "", + "licenseUrl": "", + "tooling": { + "defaultNamespace": "Microsoft.AspNet.NodeServices.Angular" + }, + "frameworks": { + "dnx451": {}, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-*", + "System.Collections": "4.0.11-beta-*", + "System.Linq": "4.0.1-beta-*", + "System.Runtime": "4.0.21-beta-*", + "System.Threading": "4.0.11-beta-*" + } + } + }, + "dependencies": { + "Microsoft.AspNet.NodeServices": "1.0.0-alpha1", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" + }, + "resource": [ + "Content/**/*" + ] +} \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices.React/.gitignore b/Microsoft.AspNet.NodeServices.React/.gitignore new file mode 100644 index 000000000000..ae3c1726048c --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js new file mode 100644 index 000000000000..27e97c7b9c29 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js @@ -0,0 +1,40 @@ +var fs = require('fs'); +var path = require('path'); +var React = require('react'); +var ReactDOMServer = require('react-dom/server'); +var createMemoryHistory = require('history/lib/createMemoryHistory'); +var babelCore = require('babel-core'); +var babelConfig = {}; + +var origJsLoader = require.extensions['.js']; +require.extensions['.js'] = loadViaBabel; +require.extensions['.jsx'] = loadViaBabel; + +function loadViaBabel(module, filename) { + // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are. + // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict + // mode when the developer didn't expect that (e.g., current versions of underscore.js can't be loaded in strict mode). + var useBabel = filename.indexOf('node_modules') < 0; + if (useBabel) { + var transformedFile = babelCore.transformFileSync(filename, babelConfig); + return module._compile(transformedFile.code, filename); + } else { + return origJsLoader.apply(this, arguments); + } +} + +module.exports = { + renderToString: function(callback, options) { + var resolvedPath = path.resolve(process.cwd(), options.moduleName); + var requestedModule = require(resolvedPath); + var component = requestedModule[options.exportName]; + if (!component) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"'); + } + + var history = createMemoryHistory(options.baseUrl); + var reactElement = React.createElement(component, { history: history }); + var html = ReactDOMServer.renderToString(reactElement); + callback(null, html); + } +}; diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs new file mode 100644 index 000000000000..653e52ae31ff --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices.React +{ + public static class ReactRenderer + { + private static StringAsTempFile nodeScript; + private static NodeInstance nodeInstance = new NodeInstance(); + + static ReactRenderer() { + // Consider populating this lazily + var script = EmbeddedResourceReader.Read(typeof (ReactRenderer), "/Content/Node/react-rendering.js"); + nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + } + + public static async Task RenderToString(string moduleName, string exportName, string baseUrl) { + return await nodeInstance.InvokeExport(nodeScript.FileName, "renderToString", new { + moduleName, + exportName, + baseUrl + }); + } + } +} diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json new file mode 100644 index 000000000000..3500b7301170 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -0,0 +1,33 @@ +{ + "version": "1.0.0-alpha1", + "description": "Microsoft.AspNet.NodeServices.React Class Library", + "authors": [ + "Microsoft" + ], + "tags": [ + "" + ], + "projectUrl": "", + "licenseUrl": "", + "tooling": { + "defaultNamespace": "Microsoft.AspNet.NodeServices.React" + }, + "frameworks": { + "dnx451": {}, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-*", + "System.Collections": "4.0.11-beta-*", + "System.Linq": "4.0.1-beta-*", + "System.Runtime": "4.0.21-beta-*", + "System.Threading": "4.0.11-beta-*" + } + } + }, + "dependencies": { + "Microsoft.AspNet.NodeServices": "1.0.0-alpha1" + }, + "resource": [ + "Content/**/*" + ] +} \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices/.gitignore b/Microsoft.AspNet.NodeServices/.gitignore new file mode 100644 index 000000000000..ae3c1726048c --- /dev/null +++ b/Microsoft.AspNet.NodeServices/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js new file mode 100644 index 000000000000..35ed458804bf --- /dev/null +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -0,0 +1,62 @@ +var path = require('path'); +var express = require('express'); +var bodyParser = require('body-parser') +var requestedPortOrZero = parseInt(process.argv[2]) || 0; // 0 means 'let the OS decide' + +autoQuitOnFileChange(process.cwd(), ['.js', '.json', '.html']); + +var app = express(); +app.use(bodyParser.json()); + +app.all('/', function (req, res) { + var resolvedPath = path.resolve(process.cwd(), req.body.moduleName); + var invokedModule = require(resolvedPath); + var func = req.body.exportedFunctionName ? invokedModule[req.body.exportedFunctionName] : invokedModule; + if (!func) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + req.body.exportedFunctionName + '"'); + } + + var hasSentResult = false; + var callback = function(errorValue, successValue) { + if (!hasSentResult) { + hasSentResult = true; + if (errorValue) { + res.status(500).send(errorValue); + } else { + sendResult(res, successValue); + } + } + }; + + func.apply(null, [callback].concat(req.body.args)); +}); + +var listener = app.listen(requestedPortOrZero, 'localhost', function () { + // Signal to HttpNodeHost which port it should make its HTTP connections on + console.log('[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port ' + listener.address().port + '\]'); + + // Signal to the NodeServices base class that we're ready to accept invocations + console.log('[Microsoft.AspNet.NodeServices:Listening]'); +}); + +function sendResult(response, result) { + if (typeof result === 'object') { + response.json(result); + } else { + response.send(result); + } +} + +function autoQuitOnFileChange(rootDir, extensions) { + // Note: This will only work on Windows/OS X, because the 'recursive' option isn't supported on Linux. + // Consider using a different watch mechanism (though ideally without forcing further NPM dependencies). + var fs = require('fs'); + var path = require('path'); + fs.watch(rootDir, { persistent: false, recursive: true }, function(event, filename) { + var ext = path.extname(filename); + if (extensions.indexOf(ext) >= 0) { + console.log('Restarting due to file change: ' + filename); + process.exit(0); + } + }); +} diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js new file mode 100644 index 000000000000..7f5218e9ad58 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js @@ -0,0 +1,23 @@ +var path = require('path'); +var readline = require('readline'); +var invocationPrefix = 'invoke:'; + +function invocationCallback(errorValue, successValue) { + if (errorValue) { + throw new Error('InputOutputStreamHost doesn\'t support errors. Got error: ' + errorValue.toString()); + } else { + var serializedResult = typeof successValue === 'object' ? JSON.stringify(successValue) : successValue; + console.log(serializedResult); + } +} + +readline.createInterface({ input: process.stdin }).on('line', function (message) { + if (message && message.substring(0, invocationPrefix.length) === invocationPrefix) { + var invocation = JSON.parse(message.substring(invocationPrefix.length)); + var invokedModule = require(path.resolve(process.cwd(), invocation.moduleName)); + var func = invocation.exportedFunctionName ? invokedModule[invocation.exportedFunctionName] : invokedModule; + func.apply(null, [invocationCallback].concat(invocation.args)); + } +}); + +console.log('[Microsoft.AspNet.NodeServices:Listening]'); // The .NET app waits for this signal before sending any invocations diff --git a/Microsoft.AspNet.NodeServices/HostingModels/EmbeddedResourceReader.cs b/Microsoft.AspNet.NodeServices/HostingModels/EmbeddedResourceReader.cs new file mode 100644 index 000000000000..4369ea79c007 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/HostingModels/EmbeddedResourceReader.cs @@ -0,0 +1,17 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Microsoft.AspNet.NodeServices { + public static class EmbeddedResourceReader { + public static string Read(Type assemblyContainingType, string path) { + var asm = assemblyContainingType.GetTypeInfo().Assembly; + var embeddedResourceName = asm.GetName().Name + path.Replace("/", "."); + + using (var stream = asm.GetManifestResourceStream(embeddedResourceName)) + using (var sr = new StreamReader(stream)) { + return sr.ReadToEnd(); + } + } + } +} \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs new file mode 100644 index 000000000000..14dd3ef6e3c7 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs @@ -0,0 +1,50 @@ +using System.Net.Http; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.NodeServices { + internal class HttpNodeHost : OutOfProcessNodeRunner { + private readonly static Regex PortMessageRegex = new Regex(@"^\[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); + + private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + private int _portNumber; + + public HttpNodeHost(int port = 0) + : base(EmbeddedResourceReader.Read(typeof(HttpNodeHost), "/Content/Node/entrypoint-http.js"), port.ToString()) + { + } + + public override async Task Invoke(NodeInvocationInfo invocationInfo) { + await this.EnsureReady(); + + using (var client = new HttpClient()) { + // TODO: Use System.Net.Http.Formatting (PostAsJsonAsync etc.) + var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings); + var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); + var response = await client.PostAsync("http://localhost:" + this._portNumber, payload); + var responseString = await response.Content.ReadAsStringAsync(); + return responseString; + } + } + + protected override void OnOutputDataReceived(string outputData) { + var match = this._portNumber != 0 ? null : PortMessageRegex.Match(outputData); + if (match != null && match.Success) { + this._portNumber = int.Parse(match.Groups[1].Captures[0].Value); + } else { + base.OnOutputDataReceived(outputData); + } + } + + protected override void OnBeforeLaunchProcess() { + // Prepare to receive a new port number + this._portNumber = 0; + } + } +} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs new file mode 100644 index 000000000000..f8a7ca793882 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Microsoft.AspNet.NodeServices { + // This is just to demonstrate that other transports are possible. This implementation is extremely + // dubious - if the Node-side code fails to conform to the expected protocol in any way (e.g., has an + // error), then it will just hang forever. So don't use this. + // + // But it's fast - the communication round-trip time is about 0.2ms (tested on OS X on a recent machine), + // versus 2-3ms for the HTTP transport. + // + // Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes + // on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the + // requests, associating them with responses, and scheduling use of the comms channel. + internal class InputOutputStreamNodeHost : OutOfProcessNodeRunner + { + private SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); + private TaskCompletionSource _currentInvocationResult; + + private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { + ContractResolver = new CamelCasePropertyNamesContractResolver() + }; + + public InputOutputStreamNodeHost() + : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeHost), "/Content/Node/entrypoint-stream.js")) + { + } + + public override async Task Invoke(NodeInvocationInfo invocationInfo) { + await this._invocationSemaphore.WaitAsync(); + try { + await this.EnsureReady(); + + var payloadJson = JsonConvert.SerializeObject(invocationInfo, jsonSerializerSettings); + var nodeProcess = this.NodeProcess; + this._currentInvocationResult = new TaskCompletionSource(); + nodeProcess.StandardInput.Write("\ninvoke:"); + nodeProcess.StandardInput.WriteLine(payloadJson); // WriteLineAsync isn't supported cross-platform + return await this._currentInvocationResult.Task; + } finally { + this._invocationSemaphore.Release(); + this._currentInvocationResult = null; + } + } + + protected override void OnOutputDataReceived(string outputData) { + if (this._currentInvocationResult != null) { + this._currentInvocationResult.SetResult(outputData); + } else { + base.OnOutputDataReceived(outputData); + } + } + } +} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs b/Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs new file mode 100644 index 000000000000..a66f9787f2c6 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices { + public abstract class NodeHost : System.IDisposable + { + public abstract Task Invoke(NodeInvocationInfo invocationInfo); + + public abstract void Dispose(); + } +} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/NodeInvocationInfo.cs b/Microsoft.AspNet.NodeServices/HostingModels/NodeInvocationInfo.cs new file mode 100644 index 000000000000..682877f47e67 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/HostingModels/NodeInvocationInfo.cs @@ -0,0 +1,8 @@ +namespace Microsoft.AspNet.NodeServices { + public class NodeInvocationInfo + { + public string ModuleName; + public string ExportedFunctionName; + public object[] Args; + } +} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs new file mode 100644 index 000000000000..fb3d06f962ce --- /dev/null +++ b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs @@ -0,0 +1,133 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices { + /** + * Class responsible for launching the Node child process, determining when it is ready to accept invocations, + * and finally killing it when the parent process exits. Also it restarts the child process if it dies. + */ + internal abstract class OutOfProcessNodeRunner : NodeHost { + private object _childProcessLauncherLock; + private bool disposed; + private StringAsTempFile _entryPointScript; + private string _commandLineArguments; + private Process _nodeProcess; + private TaskCompletionSource _nodeProcessIsReadySource; + + protected Process NodeProcess { + get { + // This is only exposed to support the UnreliableStreamNodeHost, which is just to verify that + // other hosting/transport mechanisms are possible. This shouldn't really be exposed. + return this._nodeProcess; + } + } + + public OutOfProcessNodeRunner(string entryPointScript, string commandLineArguments = null) + { + this._childProcessLauncherLock = new object(); + this._entryPointScript = new StringAsTempFile(entryPointScript); + this._commandLineArguments = commandLineArguments ?? string.Empty; + } + + protected async Task EnsureReady() { + lock (this._childProcessLauncherLock) { + if (this._nodeProcess == null || this._nodeProcess.HasExited) { + var startInfo = new ProcessStartInfo("node") { + Arguments = this._entryPointScript.FileName + " " + this._commandLineArguments, + UseShellExecute = false, + RedirectStandardInput = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + // Append current directory to NODE_PATH so it can locate node_modules + var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; + if (existingNodePath != string.Empty) { + existingNodePath += ":"; + } + + var nodePathValue = existingNodePath + Path.Combine(Directory.GetCurrentDirectory(), "node_modules"); + #if DNX451 + startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue); + #else + startInfo.Environment.Add("NODE_PATH", nodePathValue); + #endif + + this.OnBeforeLaunchProcess(); + this._nodeProcess = Process.Start(startInfo); + this.ConnectToInputOutputStreams(); + } + } + + var initializationSucceeded = await this._nodeProcessIsReadySource.Task; + if (!initializationSucceeded) { + throw new InvalidOperationException("The Node.js process failed to initialize"); + } + } + + private void ConnectToInputOutputStreams() { + var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) + this._nodeProcessIsReadySource = new TaskCompletionSource(); + + this._nodeProcess.OutputDataReceived += (sender, evt) => { + if (evt.Data == "[Microsoft.AspNet.NodeServices:Listening]" && !initializationIsCompleted) { + this._nodeProcessIsReadySource.SetResult(true); + initializationIsCompleted = true; + } else if (evt.Data != null) { + this.OnOutputDataReceived(evt.Data); + } + }; + + this._nodeProcess.ErrorDataReceived += (sender, evt) => { + if (evt.Data != null) { + this.OnErrorDataReceived(evt.Data); + if (!initializationIsCompleted) { + this._nodeProcessIsReadySource.SetResult(false); + initializationIsCompleted = true; + } + } + }; + + this._nodeProcess.BeginOutputReadLine(); + this._nodeProcess.BeginErrorReadLine(); + } + + protected virtual void OnBeforeLaunchProcess() { + } + + protected virtual void OnOutputDataReceived(string outputData) { + Console.WriteLine("[Node] " + outputData); + } + + protected virtual void OnErrorDataReceived(string errorData) { + Console.WriteLine("[Node] " + errorData); + } + + public override void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposed) { + if (disposing) { + this._entryPointScript.Dispose(); + } + + if (this._nodeProcess != null && !this._nodeProcess.HasExited) { + this._nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? System.Console.WriteLine("Killed"); + } + + disposed = true; + } + } + + ~OutOfProcessNodeRunner() { + Dispose (false); + } + } +} \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices/NodeHostingModel.cs b/Microsoft.AspNet.NodeServices/NodeHostingModel.cs new file mode 100644 index 000000000000..49a8fc510f6c --- /dev/null +++ b/Microsoft.AspNet.NodeServices/NodeHostingModel.cs @@ -0,0 +1,6 @@ +namespace Microsoft.AspNet.NodeServices { + public enum NodeHostingModel { + Http, + InputOutputStream, + } +} \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices/NodeInstance.cs b/Microsoft.AspNet.NodeServices/NodeInstance.cs new file mode 100644 index 000000000000..7768f18d1d23 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/NodeInstance.cs @@ -0,0 +1,38 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices { + public class NodeInstance : IDisposable { + private readonly NodeHost _nodeHost; + + public NodeInstance(NodeHostingModel hostingModel = NodeHostingModel.Http) { + switch (hostingModel) { + case NodeHostingModel.Http: + this._nodeHost = new HttpNodeHost(); + break; + case NodeHostingModel.InputOutputStream: + this._nodeHost = new InputOutputStreamNodeHost(); + break; + default: + throw new ArgumentException("Unknown hosting model: " + hostingModel.ToString()); + } + } + + public Task Invoke(string moduleName, params object[] args) { + return this.InvokeExport(moduleName, null, args); + } + + public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { + return await this._nodeHost.Invoke(new NodeInvocationInfo { + ModuleName = moduleName, + ExportedFunctionName = exportedFunctionName, + Args = args + }); + } + + public void Dispose() + { + this._nodeHost.Dispose(); + } + } +} diff --git a/Microsoft.AspNet.NodeServices/StringAsTempFile.cs b/Microsoft.AspNet.NodeServices/StringAsTempFile.cs new file mode 100644 index 000000000000..023e74316672 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/StringAsTempFile.cs @@ -0,0 +1,39 @@ +using System; +using System.IO; + +namespace Microsoft.AspNet.NodeServices { + // Makes it easier to pass script files to Node in a way that's sure to clean up after the process exits + public sealed class StringAsTempFile : IDisposable { + public string FileName { get; private set; } + + private bool _disposedValue; + + public StringAsTempFile(string content) { + this.FileName = Path.GetTempFileName(); + File.WriteAllText(this.FileName, content); + } + + private void DisposeImpl(bool disposing) + { + if (!_disposedValue) { + if (disposing) { + // TODO: dispose managed state (managed objects). + } + + File.Delete(this.FileName); + + _disposedValue = true; + } + } + + public void Dispose() + { + DisposeImpl(true); + GC.SuppressFinalize(this); + } + + ~StringAsTempFile() { + DisposeImpl(false); + } + } +} diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json new file mode 100644 index 000000000000..d94a2107bf59 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/project.json @@ -0,0 +1,34 @@ +{ + "version": "1.0.0-alpha1", + "description": "Microsoft.AspNet.NodeServices", + "authors": [ "Microsoft" ], + "tags": [""], + "projectUrl": "", + "licenseUrl": "", + + "dependencies": { + "System.Net.Http": "4.0.1-beta-23409", + "Newtonsoft.Json": "8.0.1-beta1" + }, + + "frameworks": { + "dnx451": { }, + "dnxcore50": { + "dependencies": { + "Microsoft.CSharp": "4.0.1-beta-23217", + "System.Collections": "4.0.11-beta-23217", + "System.Linq": "4.0.1-beta-23217", + "System.Runtime": "4.0.21-beta-23217", + "System.Threading": "4.0.11-beta-23217", + "System.Text.RegularExpressions": "4.0.11-beta-23409", + "System.Diagnostics.Process": "4.1.0-beta-23409", + "System.IO.FileSystem": "4.0.1-beta-23409", + "System.Console": "4.0.0-beta-23409" + } + } + }, + + "resource": [ + "Content/**/*" + ] +} diff --git a/samples/angular/MusicStore/.gitignore b/samples/angular/MusicStore/.gitignore new file mode 100644 index 000000000000..051e6c1c87cd --- /dev/null +++ b/samples/angular/MusicStore/.gitignore @@ -0,0 +1,5 @@ +/node_modules/ +/wwwroot/lib/ +/wwwroot/ng-app/**/*.js +/project.lock.json +/music-db.sqlite diff --git a/samples/angular/MusicStore/Apis/AlbumsApiController.cs b/samples/angular/MusicStore/Apis/AlbumsApiController.cs new file mode 100644 index 000000000000..fdb44647d43d --- /dev/null +++ b/samples/angular/MusicStore/Apis/AlbumsApiController.cs @@ -0,0 +1,210 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Mvc; +using Microsoft.Data.Entity; +using AutoMapper; +using MusicStore.Models; +using MusicStore.Infrastructure; + +namespace MusicStore.Apis +{ + [Route("api/albums")] + public class AlbumsApiController : Controller + { + private readonly MusicStoreContext _storeContext; + + public AlbumsApiController(MusicStoreContext storeContext) + { + _storeContext = storeContext; + } + + [HttpGet] + [NoCache] + public async Task Paged(int page = 1, int pageSize = 50, string sortBy = null) + { + await _storeContext.Genres.LoadAsync(); + await _storeContext.Artists.LoadAsync(); + + var albums = await _storeContext.Albums + // .Include(a => a.Genre) + // .Include(a => a.Artist) + .ToPagedListAsync(page, pageSize, sortBy, + a => a.Title, // sortExpression + SortDirection.Ascending, // defaultSortDirection + a => Mapper.Map(a, new AlbumResultDto())); // selector + + return Json(albums); + } + + [HttpGet("all")] + [NoCache] + public async Task All() + { + var albums = await _storeContext.Albums + //.Include(a => a.Genre) + //.Include(a => a.Artist) + .OrderBy(a => a.Title) + .ToListAsync(); + + return Json(albums.Select(a => Mapper.Map(a, new AlbumResultDto()))); + } + + [HttpGet("mostPopular")] + [NoCache] + public async Task MostPopular(int count = 6) + { + count = count > 0 && count < 20 ? count : 6; + var albums = await _storeContext.Albums + .OrderByDescending(a => a.OrderDetails.Count()) + .Take(count) + .ToListAsync(); + + // TODO: Move the .Select() to end of albums query when EF supports it + return Json(albums.Select(a => Mapper.Map(a, new AlbumResultDto()))); + } + + [HttpGet("{albumId:int}")] + [NoCache] + public async Task Details(int albumId) + { + await _storeContext.Genres.LoadAsync(); + await _storeContext.Artists.LoadAsync(); + + var album = await _storeContext.Albums + //.Include(a => a.Artist) + //.Include(a => a.Genre) + .Where(a => a.AlbumId == albumId) + .SingleOrDefaultAsync(); + + var albumResult = Mapper.Map(album, new AlbumResultDto()); + + // TODO: Get these from the related entities when EF supports that again, i.e. when .Include() works + //album.Artist.Name = (await _storeContext.Artists.SingleOrDefaultAsync(a => a.ArtistId == album.ArtistId)).Name; + //album.Genre.Name = (await _storeContext.Genres.SingleOrDefaultAsync(g => g.GenreId == album.GenreId)).Name; + + // TODO: Add null checking and return 404 in that case + + return Json(albumResult); + } + + [HttpPost] + [Authorize("app-ManageStore")] + public async Task CreateAlbum([FromBody]AlbumChangeDto album) + { + if (!ModelState.IsValid) + { + // Return the model errors + return new ApiResult(ModelState); + } + + // Save the changes to the DB + var dbAlbum = new Album(); + _storeContext.Albums.Add(Mapper.Map(album, dbAlbum)); + await _storeContext.SaveChangesAsync(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + + return new ApiResult + { + Data = dbAlbum.AlbumId, + Message = "Album created successfully." + }; + } + + [HttpPut("{albumId:int}/update")] + [Authorize("app-ManageStore")] + public async Task UpdateAlbum(int albumId, [FromBody]AlbumChangeDto album) + { + if (!ModelState.IsValid) + { + // Return the model errors + return new ApiResult(ModelState); + } + + var dbAlbum = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId); + + if (dbAlbum == null) + { + return new ApiResult + { + StatusCode = 404, + Message = string.Format("The album with ID {0} was not found.", albumId) + }; + } + + // Save the changes to the DB + Mapper.Map(album, dbAlbum); + await _storeContext.SaveChangesAsync(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + + return new ApiResult + { + Message = "Album updated successfully." + }; + } + + [HttpDelete("{albumId:int}")] + [Authorize("app-ManageStore")] + public async Task DeleteAlbum(int albumId) + { + var album = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId); + //var album = _storeContext.Albums.SingleOrDefault(a => a.AlbumId == albumId); + + if (album != null) + { + _storeContext.Albums.Remove(album); + + // Save the changes to the DB + await _storeContext.SaveChangesAsync(); + + // TODO: Handle missing record, key violations, concurrency issues, etc. + } + + return new ApiResult + { + Message = "Album deleted successfully." + }; + } + } + + [ModelMetadataType(typeof(Album))] + public class AlbumChangeDto + { + public int GenreId { get; set; } + + public int ArtistId { get; set; } + + public string Title { get; set; } + + public decimal Price { get; set; } + + public string AlbumArtUrl { get; set; } + } + + public class AlbumResultDto : AlbumChangeDto + { + public AlbumResultDto() + { + Artist = new ArtistResultDto(); + Genre = new GenreResultDto(); + } + + public int AlbumId { get; set; } + + public ArtistResultDto Artist { get; private set; } + + public GenreResultDto Genre { get; private set; } + } + + public class ArtistResultDto + { + public string Name { get; set; } + } + + public class GenreResultDto + { + public string Name { get; set; } + } +} diff --git a/samples/angular/MusicStore/Apis/ArtistsApiController.cs b/samples/angular/MusicStore/Apis/ArtistsApiController.cs new file mode 100644 index 000000000000..0efe1109198d --- /dev/null +++ b/samples/angular/MusicStore/Apis/ArtistsApiController.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.Data.Entity; +using MusicStore.Models; + +namespace MusicStore.Apis +{ + [Route("api/artists")] + public class ArtistsApiController : Controller + { + private readonly MusicStoreContext _storeContext; + + public ArtistsApiController(MusicStoreContext storeContext) + { + _storeContext = storeContext; + } + + [HttpGet("lookup")] + public async Task Lookup() + { + var artists = await _storeContext.Artists + .OrderBy(a => a.Name) + .ToListAsync(); + + return Json(artists); + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/GenresApiController.cs b/samples/angular/MusicStore/Apis/GenresApiController.cs new file mode 100644 index 000000000000..e5d6ba109165 --- /dev/null +++ b/samples/angular/MusicStore/Apis/GenresApiController.cs @@ -0,0 +1,70 @@ +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.Data.Entity; +using MusicStore.Models; +using MusicStore.Infrastructure; + +namespace MusicStore.Apis +{ + [Route("api/genres")] + public class GenresApiController : Controller + { + private readonly MusicStoreContext _storeContext; + + public GenresApiController(MusicStoreContext storeContext) + { + _storeContext = storeContext; + } + + [HttpGet] + public async Task GenreList() + { + var genres = await _storeContext.Genres + //.Include(g => g.Albums) + .OrderBy(g => g.Name) + .ToListAsync(); + + return Json(genres); + } + + [HttpGet("genre-lookup")] + public async Task Lookup() + { + var genres = await _storeContext.Genres + .Select(g => new { g.GenreId, g.Name }) + .ToListAsync(); + + return Json(genres); + } + + [HttpGet("menu")] + public async Task GenreMenuList(int count = 9) + { + count = count > 0 && count < 20 ? count : 9; + + var genres = await _storeContext.Genres + .OrderByDescending(g => + g.Albums.Sum(a => + a.OrderDetails.Sum(od => od.Quantity))) + .Take(count) + .ToListAsync(); + + return Json(genres); + } + + [HttpGet("{genreId:int}/albums")] + [NoCache] + public async Task GenreAlbums(int genreId) + { + var albums = await _storeContext.Albums + .Where(a => a.GenreId == genreId) + //.Include(a => a.Genre) + //.Include(a => a.Artist) + //.OrderBy(a => a.Genre.Name) + .ToListAsync(); + + return Json(albums); + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/AccountViewModels.cs b/samples/angular/MusicStore/Apis/Models/AccountViewModels.cs new file mode 100644 index 000000000000..657a4f011dbd --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/AccountViewModels.cs @@ -0,0 +1,63 @@ +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + } + + public class ManageUserViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class LoginViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterViewModel + { + [Required] + [Display(Name = "User name")] + public string UserName { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/Album.cs b/samples/angular/MusicStore/Apis/Models/Album.cs new file mode 100644 index 000000000000..35f03d9f821e --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/Album.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class Album + { + public Album() + { + // TODO: Temporary hack to populate the orderdetails until EF does this automatically. + OrderDetails = new List(); + } + + [ScaffoldColumn(false)] + public int AlbumId { get; set; } + + public int GenreId { get; set; } + + public int ArtistId { get; set; } + + [Required] + [StringLength(160, MinimumLength = 2)] + public string Title { get; set; } + + [Required] + [Range(0.01, 100.00)] + [DataType(DataType.Currency)] + public decimal Price { get; set; } + + [Display(Name = "Album Art URL")] + [StringLength(1024)] + public string AlbumArtUrl { get; set; } + + public virtual Genre Genre { get; set; } + + public virtual Artist Artist { get; set; } + + public virtual ICollection OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/Artist.cs b/samples/angular/MusicStore/Apis/Models/Artist.cs new file mode 100644 index 000000000000..43d677c43796 --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/Artist.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class Artist + { + public int ArtistId { get; set; } + + [Required] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/CartItem.cs b/samples/angular/MusicStore/Apis/Models/CartItem.cs new file mode 100644 index 000000000000..64550abf9a70 --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/CartItem.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + public class CartItem + { + [Key] + public int CartItemId { get; set; } + + [Required] + public string CartId { get; set; } + public int AlbumId { get; set; } + public int Count { get; set; } + + [DataType(DataType.DateTime)] + public DateTime DateCreated { get; set; } + + public virtual Album Album { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/Genre.cs b/samples/angular/MusicStore/Apis/Models/Genre.cs new file mode 100644 index 000000000000..a7fdb34f4139 --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/Genre.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using Newtonsoft.Json; + +namespace MusicStore.Models +{ + public class Genre + { + public Genre() + { + Albums = new List(); + } + + public int GenreId { get; set; } + + [Required] + public string Name { get; set; } + + public string Description { get; set; } + + [JsonIgnore] + public virtual ICollection Albums { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs b/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs new file mode 100644 index 000000000000..d63ff692b5af --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs @@ -0,0 +1,39 @@ +using System; +using System.Linq; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Metadata; +using Microsoft.Framework.OptionsModel; + +namespace MusicStore.Models +{ + public class ApplicationUser : IdentityUser { } + + public class MusicStoreContext : IdentityDbContext + { + public MusicStoreContext() + { + } + + public DbSet Albums { get; set; } + public DbSet Artists { get; set; } + public DbSet Orders { get; set; } + public DbSet Genres { get; set; } + public DbSet CartItems { get; set; } + public DbSet OrderDetails { get; set; } + + protected override void OnModelCreating(ModelBuilder builder) + { + // Configure pluralization + builder.Entity().ToTable("Albums"); + builder.Entity().ToTable("Artists"); + builder.Entity().ToTable("Orders"); + builder.Entity().ToTable("Genres"); + builder.Entity().ToTable("CartItems"); + builder.Entity().ToTable("OrderDetails"); + + base.OnModelCreating(builder); + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/Order.cs b/samples/angular/MusicStore/Apis/Models/Order.cs new file mode 100644 index 000000000000..0461dbce8a2a --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/Order.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace MusicStore.Models +{ + //[Bind(Include = "FirstName,LastName,Address,City,State,PostalCode,Country,Phone,Email")] + public class Order + { + public Order() + { + OrderDetails = new List(); + } + + [ScaffoldColumn(false)] + public int OrderId { get; set; } + + [ScaffoldColumn(false)] + public DateTime OrderDate { get; set; } + + [Required] + [ScaffoldColumn(false)] + public string Username { get; set; } + + [Required] + [Display(Name = "First Name")] + [StringLength(160)] + public string FirstName { get; set; } + + [Required] + [Display(Name = "Last Name")] + [StringLength(160)] + public string LastName { get; set; } + + [Required] + [StringLength(70, MinimumLength = 3)] + public string Address { get; set; } + + [Required] + [StringLength(40)] + public string City { get; set; } + + [Required] + [StringLength(40)] + public string State { get; set; } + + [Required] + [Display(Name = "Postal Code")] + [StringLength(10, MinimumLength = 5)] + public string PostalCode { get; set; } + + [Required] + [StringLength(40)] + public string Country { get; set; } + + [Required] + [StringLength(24)] + [DataType(DataType.PhoneNumber)] + public string Phone { get; set; } + + [Required] + [Display(Name = "Email Address")] + [RegularExpression(@"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}", + ErrorMessage = "Email is not valid.")] + [DataType(DataType.EmailAddress)] + public string Email { get; set; } + + [ScaffoldColumn(false)] + public decimal Total { get; set; } + + public ICollection OrderDetails { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/OrderDetail.cs b/samples/angular/MusicStore/Apis/Models/OrderDetail.cs new file mode 100644 index 000000000000..29f87988dfe6 --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/OrderDetail.cs @@ -0,0 +1,14 @@ +namespace MusicStore.Models +{ + public class OrderDetail + { + public int OrderDetailId { get; set; } + public int OrderId { get; set; } + public int AlbumId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } + + public virtual Album Album { get; set; } + public virtual Order Order { get; set; } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/SampleData.cs b/samples/angular/MusicStore/Apis/Models/SampleData.cs new file mode 100644 index 000000000000..25e2a5c97f5a --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/SampleData.cs @@ -0,0 +1,944 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.Data.Entity; +using Microsoft.Data.Entity.Storage; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.OptionsModel; +using MusicStore; +using MusicStore.Models; + +namespace MusicStore.Models +{ + public static class SampleData + { + const string imgUrl = "/images/placeholder.png"; + + public static async Task InitializeMusicStoreDatabaseAsync(IServiceProvider serviceProvider) + { + using (var db = serviceProvider.GetService()) + { + if (await db.Database.EnsureCreatedAsync()) + { + await InsertTestData(serviceProvider); + await CreateAdminUser(serviceProvider); + } + } + } + + private static async Task CreateAdminUser(IServiceProvider serviceProvider) + { + return; + + var settings = serviceProvider.GetService>().Value; + const string adminRole = "Administrator"; + + var userManager = serviceProvider.GetService>(); + var roleManager = serviceProvider.GetService>(); + + if (!await roleManager.RoleExistsAsync(adminRole)) + { + await roleManager.CreateAsync(new IdentityRole(adminRole)); + } + + var user = await userManager.FindByNameAsync(settings.DefaultAdminUsername); + if (user == null) + { + user = new ApplicationUser { UserName = settings.DefaultAdminUsername }; + await userManager.CreateAsync(user, settings.DefaultAdminPassword); + await userManager.AddToRoleAsync(user, adminRole); + await userManager.AddClaimAsync(user, new Claim("app-ManageStore", "Allowed")); + } + } + + private static async Task InsertTestData(IServiceProvider serviceProvider) + { + var albums = GetAlbums(imgUrl, Genres, Artists); + await AddOrUpdateAsync(serviceProvider, g => g.GenreId, Genres.Select(genre => genre.Value)); + await AddOrUpdateAsync(serviceProvider, a => a.ArtistId, Artists.Select(artist => artist.Value)); + await AddOrUpdateAsync(serviceProvider, a => a.AlbumId, albums); + } + + // TODO [EF] This may be replaced by a first class mechanism in EF + private static async Task AddOrUpdateAsync( + IServiceProvider serviceProvider, + Func propertyToMatch, IEnumerable entities) + where TEntity : class + { + // Query in a separate context so that we can attach existing entities as modified + List existingData; + + using (var scope = serviceProvider.GetRequiredService().CreateScope()) + using (var db = scope.ServiceProvider.GetService()) + { + existingData = db.Set().ToList(); + } + + using (var scope = serviceProvider.GetRequiredService().CreateScope()) + using (var db = scope.ServiceProvider.GetService()) + { + foreach (var item in entities) + { + db.Entry(item).State = existingData.Any(g => propertyToMatch(g).Equals(propertyToMatch(item))) + ? EntityState.Modified + : EntityState.Added; + } + + await db.SaveChangesAsync(); + } + } + + private static Album[] GetAlbums(string imgUrl, Dictionary genres, Dictionary artists) + { + var albums = new Album[] + { + new Album { Title = "The Best Of The Men At Work", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Men At Work"], AlbumArtUrl = imgUrl }, + new Album { Title = "...And Justice For All", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "עד גבול האור", Genre = genres["World"], Price = 8.99M, Artist = artists["אריק אינשטיין"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Light Syndrome", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Terry Bozzio, Tony Levin & Steve Stevens"], AlbumArtUrl = imgUrl }, + new Album { Title = "10,000 Days", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "11i", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Supreme Beings of Leisure"], AlbumArtUrl = imgUrl }, + new Album { Title = "1960", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Soul-Junk"], AlbumArtUrl = imgUrl }, + new Album { Title = "4x4=12 ", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["deadmau5"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Copland Celebration, Vol. I", Genre = genres["Classical"], Price = 8.99M, Artist = artists["London Symphony Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Lively Mind", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Paul Oakenfold"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Matter of Life and Death", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Real Dead One", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Real Live One", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Rush of Blood to the Head", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Coldplay"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Soprano Inspired", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Britten Sinfonia, Ivor Bolton & Lesley Garrett"], AlbumArtUrl = imgUrl }, + new Album { Title = "A Winter Symphony", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Abbey Road", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Beatles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ace Of Spades", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Motörhead"], AlbumArtUrl = imgUrl }, + new Album { Title = "Achtung Baby", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Acústico MTV", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Os Paralamas Do Sucesso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Adams, John: The Chairman Dances", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Edo de Waart & San Francisco Symphony"], AlbumArtUrl = imgUrl }, + new Album { Title = "Adrenaline", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deftones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ænima", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "Afrociberdelia", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Chico Science & Nação Zumbi"], AlbumArtUrl = imgUrl }, + new Album { Title = "After the Goldrush", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Neil Young"], AlbumArtUrl = imgUrl }, + new Album { Title = "Airdrawn Dagger", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Sasha"], AlbumArtUrl = imgUrl }, + new Album { Title = "Album Title Goes Here", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["deadmau5"], AlbumArtUrl = imgUrl }, + new Album { Title = "Alcohol Fueled Brewtality Live! [Disc 1]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Label Society"], AlbumArtUrl = imgUrl }, + new Album { Title = "Alcohol Fueled Brewtality Live! [Disc 2]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Label Society"], AlbumArtUrl = imgUrl }, + new Album { Title = "Alive 2007", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Daft Punk"], AlbumArtUrl = imgUrl }, + new Album { Title = "All I Ask of You", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Amen (So Be It)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Paddy Casey"], AlbumArtUrl = imgUrl }, + new Album { Title = "Animal Vehicle", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Axis of Awesome"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ao Vivo [IMPORT]", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Zeca Pagodinho"], AlbumArtUrl = imgUrl }, + new Album { Title = "Apocalyptic Love", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Slash"], AlbumArtUrl = imgUrl }, + new Album { Title = "Appetite for Destruction", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Are You Experienced?", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Jimi Hendrix"], AlbumArtUrl = imgUrl }, + new Album { Title = "Arquivo II", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Os Paralamas Do Sucesso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Arquivo Os Paralamas Do Sucesso", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Os Paralamas Do Sucesso"], AlbumArtUrl = imgUrl }, + new Album { Title = "A-Sides", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Soundgarden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Audioslave", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Audioslave"], AlbumArtUrl = imgUrl }, + new Album { Title = "Automatic for the People", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["R.E.M."], AlbumArtUrl = imgUrl }, + new Album { Title = "Axé Bahia 2001", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Various Artists"], AlbumArtUrl = imgUrl }, + new Album { Title = "Babel", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Mumford & Sons"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: Goldberg Variations", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Wilhelm Kempff"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: The Brandenburg Concertos", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Orchestra of The Age of Enlightenment"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: The Cello Suites", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Yo-Yo Ma"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bach: Toccata & Fugue in D Minor", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Ton Koopman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bad Motorfinger", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Soundgarden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Balls to the Wall", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Accept"], AlbumArtUrl = imgUrl }, + new Album { Title = "Banadeek Ta'ala", Genre = genres["World"], Price = 8.99M, Artist = artists["Amr Diab"], AlbumArtUrl = imgUrl }, + new Album { Title = "Barbie Girl", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Aqua"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bark at the Moon (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bartok: Violin & Viola Concertos", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Yehudi Menuhin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Barulhinho Bom", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Marisa Monte"], AlbumArtUrl = imgUrl }, + new Album { Title = "BBC Sessions [Disc 1] [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "BBC Sessions [Disc 2] [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Be Here Now", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Oasis"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bedrock 11 Compiled & Mixed", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["John Digweed"], AlbumArtUrl = imgUrl }, + new Album { Title = "Berlioz: Symphonie Fantastique", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Michael Tilson Thomas"], AlbumArtUrl = imgUrl }, + new Album { Title = "Beyond Good And Evil", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Cult"], AlbumArtUrl = imgUrl }, + new Album { Title = "Big Bad Wolf ", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Armand Van Helden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Big Ones", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Aerosmith"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Album", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Sabbath Vol. 4 (Remaster)", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Sabbath"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black Sabbath", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Black Sabbath"], AlbumArtUrl = imgUrl }, + new Album { Title = "Black", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blackwater Park", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Opeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blizzard of Ozz", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blood", Genre = genres["Rock"], Price = 8.99M, Artist = artists["In This Moment"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blue Moods", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Incognito"], AlbumArtUrl = imgUrl }, + new Album { Title = "Blue", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Weezer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bongo Fury", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Frank Zappa & Captain Beefheart"], AlbumArtUrl = imgUrl }, + new Album { Title = "Boys & Girls", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alabama Shakes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Brave New World", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "B-Sides 1980-1990", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Bunkka", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Paul Oakenfold"], AlbumArtUrl = imgUrl }, + new Album { Title = "By The Way", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Red Hot Chili Peppers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cake: B-Sides and Rarities", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Cake"], AlbumArtUrl = imgUrl }, + new Album { Title = "Californication", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Red Hot Chili Peppers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Carmina Burana", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Boston Symphony Orchestra & Seiji Ozawa"], AlbumArtUrl = imgUrl }, + new Album { Title = "Carried to Dust (Bonus Track Version)", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Calexico"], AlbumArtUrl = imgUrl }, + new Album { Title = "Carry On", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Chris Cornell"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cássia Eller - Sem Limite [Disc 1]", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Cássia Eller"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chemical Wedding", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Bruce Dickinson"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chill: Brazil (Disc 1)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Marcos Valle"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chill: Brazil (Disc 2)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Antônio Carlos Jobim"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chocolate Starfish And The Hot Dog Flavored Water", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Limp Bizkit"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chronicle, Vol. 1", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Creedence Clearwater Revival"], AlbumArtUrl = imgUrl }, + new Album { Title = "Chronicle, Vol. 2", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Creedence Clearwater Revival"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ciao, Baby", Genre = genres["Rock"], Price = 8.99M, Artist = artists["TheStart"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cidade Negra - Hits", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Cidade Negra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Classic Munkle: Turbo Edition", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Munkle"], AlbumArtUrl = imgUrl }, + new Album { Title = "Classics: The Best of Sarah Brightman", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Coda", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Come Away With Me", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Norah Jones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Come Taste The Band", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Comfort Eagle", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Cake"], AlbumArtUrl = imgUrl }, + new Album { Title = "Common Reaction", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Uh Huh Her "], AlbumArtUrl = imgUrl }, + new Album { Title = "Compositores", Genre = genres["Rock"], Price = 8.99M, Artist = artists["O Terço"], AlbumArtUrl = imgUrl }, + new Album { Title = "Contraband", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Velvet Revolver"], AlbumArtUrl = imgUrl }, + new Album { Title = "Core", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Stone Temple Pilots"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cornerstone", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Styx"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cosmicolor", Genre = genres["Rap"], Price = 8.99M, Artist = artists["M-Flo"], AlbumArtUrl = imgUrl }, + new Album { Title = "Cross", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Justice"], AlbumArtUrl = imgUrl }, + new Album { Title = "Culture of Fear", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Thievery Corporation"], AlbumArtUrl = imgUrl }, + new Album { Title = "Da Lama Ao Caos", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Chico Science & Nação Zumbi"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dakshina", Genre = genres["World"], Price = 8.99M, Artist = artists["Deva Premal"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dark Side of the Moon", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "Death Magnetic", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deep End of Down", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Above the Fold"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deep Purple In Rock", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deixa Entrar", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Falamansa"], AlbumArtUrl = imgUrl }, + new Album { Title = "Deja Vu", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Crosby, Stills, Nash, and Young"], AlbumArtUrl = imgUrl }, + new Album { Title = "Di Korpu Ku Alma", Genre = genres["World"], Price = 8.99M, Artist = artists["Lura"], AlbumArtUrl = imgUrl }, + new Album { Title = "Diary of a Madman (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Diary of a Madman", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dirt", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alice in Chains"], AlbumArtUrl = imgUrl }, + new Album { Title = "Diver Down", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Djavan Ao Vivo - Vol. 02", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Djavan"], AlbumArtUrl = imgUrl }, + new Album { Title = "Djavan Ao Vivo - Vol. 1", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Djavan"], AlbumArtUrl = imgUrl }, + new Album { Title = "Drum'n'bass for Papa", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Plug"], AlbumArtUrl = imgUrl }, + new Album { Title = "Duluth", Genre = genres["Country"], Price = 8.99M, Artist = artists["Trampled By Turtles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Dummy", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Portishead"], AlbumArtUrl = imgUrl }, + new Album { Title = "Duos II", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Luciana Souza/Romero Lubambo"], AlbumArtUrl = imgUrl }, + new Album { Title = "Earl Scruggs and Friends", Genre = genres["Country"], Price = 8.99M, Artist = artists["Earl Scruggs"], AlbumArtUrl = imgUrl }, + new Album { Title = "Eden", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "El Camino", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Black Keys"], AlbumArtUrl = imgUrl }, + new Album { Title = "Elegant Gypsy", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Al di Meola"], AlbumArtUrl = imgUrl }, + new Album { Title = "Elements Of Life", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Tiësto"], AlbumArtUrl = imgUrl }, + new Album { Title = "Elis Regina-Minha História", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Elis Regina"], AlbumArtUrl = imgUrl }, + new Album { Title = "Emergency On Planet Earth", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Jamiroquai"], AlbumArtUrl = imgUrl }, + new Album { Title = "Emotion", Genre = genres["World"], Price = 8.99M, Artist = artists["Papa Wemba"], AlbumArtUrl = imgUrl }, + new Album { Title = "English Renaissance", Genre = genres["Classical"], Price = 8.99M, Artist = artists["The King's Singers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Every Kind of Light", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Posies"], AlbumArtUrl = imgUrl }, + new Album { Title = "Faceless", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Godsmack"], AlbumArtUrl = imgUrl }, + new Album { Title = "Facelift", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alice in Chains"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fair Warning", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fear of a Black Planet", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Public Enemy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fear Of The Dark", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Feels Like Home", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Norah Jones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fireball", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Fly", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "For Those About To Rock We Salute You", Genre = genres["Rock"], Price = 8.99M, Artist = artists["AC/DC"], AlbumArtUrl = imgUrl }, + new Album { Title = "Four", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Blues Traveler"], AlbumArtUrl = imgUrl }, + new Album { Title = "Frank", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Amy Winehouse"], AlbumArtUrl = imgUrl }, + new Album { Title = "Further Down the Spiral", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Nine Inch Nails"], AlbumArtUrl = imgUrl }, + new Album { Title = "Garage Inc. (Disc 1)", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Garage Inc. (Disc 2)", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Garbage", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Garbage"], AlbumArtUrl = imgUrl }, + new Album { Title = "Good News For People Who Love Bad News", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Modest Mouse"], AlbumArtUrl = imgUrl }, + new Album { Title = "Gordon", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Barenaked Ladies"], AlbumArtUrl = imgUrl }, + new Album { Title = "Górecki: Symphony No. 3", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Adrian Leaper & Doreen de Feis"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits II", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Duck Sauce"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Lenny Kravitz"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Hits", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Lenny Kravitz"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greatest Kiss", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Kiss"], AlbumArtUrl = imgUrl }, + new Album { Title = "Greetings from Michigan", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Sufjan Stevens"], AlbumArtUrl = imgUrl }, + new Album { Title = "Group Therapy", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Above & Beyond"], AlbumArtUrl = imgUrl }, + new Album { Title = "Handel: The Messiah (Highlights)", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Scholars Baroque Ensemble"], AlbumArtUrl = imgUrl }, + new Album { Title = "Haydn: Symphonies 99 - 104", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Royal Philharmonic Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Heart of the Night", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Spyro Gyra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Heart On", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Eagles of Death Metal"], AlbumArtUrl = imgUrl }, + new Album { Title = "Holy Diver", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Dio"], AlbumArtUrl = imgUrl }, + new Album { Title = "Homework", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Daft Punk"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hot Rocks, 1964-1971 (Disc 1)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Rolling Stones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Houses Of The Holy", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "How To Dismantle An Atomic Bomb", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Human", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Projected"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hunky Dory", Genre = genres["Rock"], Price = 8.99M, Artist = artists["David Bowie"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hymns", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Projected"], AlbumArtUrl = imgUrl }, + new Album { Title = "Hysteria", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Def Leppard"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Absentia", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Porcupine Tree"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Between", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Paul Van Dyk"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Rainbows", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Radiohead"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Step", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Stevie Ray Vaughan & Double Trouble"], AlbumArtUrl = imgUrl }, + new Album { Title = "In the court of the Crimson King", Genre = genres["Rock"], Price = 8.99M, Artist = artists["King Crimson"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Through The Out Door", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Your Honor [Disc 1]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Foo Fighters"], AlbumArtUrl = imgUrl }, + new Album { Title = "In Your Honor [Disc 2]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Foo Fighters"], AlbumArtUrl = imgUrl }, + new Album { Title = "Indestructible", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Rancid"], AlbumArtUrl = imgUrl }, + new Album { Title = "Infinity", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Journey"], AlbumArtUrl = imgUrl }, + new Album { Title = "Into The Light", Genre = genres["Rock"], Price = 8.99M, Artist = artists["David Coverdale"], AlbumArtUrl = imgUrl }, + new Album { Title = "Introspective", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Pet Shop Boys"], AlbumArtUrl = imgUrl }, + new Album { Title = "Iron Maiden", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "ISAM", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Amon Tobin"], AlbumArtUrl = imgUrl }, + new Album { Title = "IV", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jagged Little Pill", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Alanis Morissette"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jagged Little Pill", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Alanis Morissette"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jorge Ben Jor 25 Anos", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Jorge Ben"], AlbumArtUrl = imgUrl }, + new Album { Title = "Jota Quest-1995", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Jota Quest"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kick", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["INXS"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kill 'Em All", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kind of Blue", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "King For A Day Fool For A Lifetime", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Faith No More"], AlbumArtUrl = imgUrl }, + new Album { Title = "Kiss", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Carly Rae Jepsen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Last Call", Genre = genres["Country"], Price = 8.99M, Artist = artists["Cayouche"], AlbumArtUrl = imgUrl }, + new Album { Title = "Le Freak", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Chic"], AlbumArtUrl = imgUrl }, + new Album { Title = "Le Tigre", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Le Tigre"], AlbumArtUrl = imgUrl }, + new Album { Title = "Led Zeppelin I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Led Zeppelin II", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Led Zeppelin III", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Let There Be Rock", Genre = genres["Rock"], Price = 8.99M, Artist = artists["AC/DC"], AlbumArtUrl = imgUrl }, + new Album { Title = "Little Earthquakes", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Tori Amos"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live [Disc 1]", Genre = genres["Blues"], Price = 8.99M, Artist = artists["The Black Crowes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live [Disc 2]", Genre = genres["Blues"], Price = 8.99M, Artist = artists["The Black Crowes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live After Death", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live At Donington 1992 (Disc 1)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live At Donington 1992 (Disc 2)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live on Earth", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["The Cat Empire"], AlbumArtUrl = imgUrl }, + new Album { Title = "Live On Two Legs [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Living After Midnight", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Judas Priest"], AlbumArtUrl = imgUrl }, + new Album { Title = "Living", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Paddy Casey"], AlbumArtUrl = imgUrl }, + new Album { Title = "Load", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Love Changes Everything", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "MacArthur Park Suite", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Donna Summer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Machine Head", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Magical Mystery Tour", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Beatles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mais Do Mesmo", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Legião Urbana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Maquinarama", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Skank"], AlbumArtUrl = imgUrl }, + new Album { Title = "Marasim", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Jagjit Singh"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mascagni: Cavalleria Rusticana", Genre = genres["Classical"], Price = 8.99M, Artist = artists["James Levine"], AlbumArtUrl = imgUrl }, + new Album { Title = "Master of Puppets", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mechanics & Mathematics", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Venus Hum"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mental Jewelry", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Live"], AlbumArtUrl = imgUrl }, + new Album { Title = "Metallics", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "meteora", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Linkin Park"], AlbumArtUrl = imgUrl }, + new Album { Title = "Meus Momentos", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Gonzaguinha"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mezmerize", Genre = genres["Metal"], Price = 8.99M, Artist = artists["System Of A Down"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mezzanine", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Massive Attack"], AlbumArtUrl = imgUrl }, + new Album { Title = "Miles Ahead", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "Milton Nascimento Ao Vivo", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Milton Nascimento"], AlbumArtUrl = imgUrl }, + new Album { Title = "Minas", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Milton Nascimento"], AlbumArtUrl = imgUrl }, + new Album { Title = "Minha Historia", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Chico Buarque"], AlbumArtUrl = imgUrl }, + new Album { Title = "Misplaced Childhood", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Marillion"], AlbumArtUrl = imgUrl }, + new Album { Title = "MK III The Final Concerts [Disc 1]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Morning Dance", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Spyro Gyra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Motley Crue Greatest Hits", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Mötley Crüe"], AlbumArtUrl = imgUrl }, + new Album { Title = "Moving Pictures", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Rush"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mozart: Chamber Music", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Nash Ensemble"], AlbumArtUrl = imgUrl }, + new Album { Title = "Mozart: Symphonies Nos. 40 & 41", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Berliner Philharmoniker"], AlbumArtUrl = imgUrl }, + new Album { Title = "Murder Ballads", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Nick Cave and the Bad Seeds"], AlbumArtUrl = imgUrl }, + new Album { Title = "Music For The Jilted Generation", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["The Prodigy"], AlbumArtUrl = imgUrl }, + new Album { Title = "My Generation - The Very Best Of The Who", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Who"], AlbumArtUrl = imgUrl }, + new Album { Title = "My Name is Skrillex", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Skrillex"], AlbumArtUrl = imgUrl }, + new Album { Title = "Na Pista", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Cláudio Zoli"], AlbumArtUrl = imgUrl }, + new Album { Title = "Nevermind", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Nirvana"], AlbumArtUrl = imgUrl }, + new Album { Title = "New Adventures In Hi-Fi", Genre = genres["Rock"], Price = 8.99M, Artist = artists["R.E.M."], AlbumArtUrl = imgUrl }, + new Album { Title = "New Divide", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Linkin Park"], AlbumArtUrl = imgUrl }, + new Album { Title = "New York Dolls", Genre = genres["Punk"], Price = 8.99M, Artist = artists["New York Dolls"], AlbumArtUrl = imgUrl }, + new Album { Title = "News Of The World", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Nielsen: The Six Symphonies", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Göteborgs Symfoniker & Neeme Järvi"], AlbumArtUrl = imgUrl }, + new Album { Title = "Night At The Opera", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Queen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Night Castle", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Trans-Siberian Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Nkolo", Genre = genres["World"], Price = 8.99M, Artist = artists["Lokua Kanza"], AlbumArtUrl = imgUrl }, + new Album { Title = "No More Tears (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "No Prayer For The Dying", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "No Security", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Rolling Stones"], AlbumArtUrl = imgUrl }, + new Album { Title = "O Brother, Where Art Thou?", Genre = genres["Country"], Price = 8.99M, Artist = artists["Alison Krauss"], AlbumArtUrl = imgUrl }, + new Album { Title = "O Samba Poconé", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Skank"], AlbumArtUrl = imgUrl }, + new Album { Title = "O(+>", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Prince"], AlbumArtUrl = imgUrl }, + new Album { Title = "Oceania", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Smashing Pumpkins"], AlbumArtUrl = imgUrl }, + new Album { Title = "Off the Deep End", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Weird Al"], AlbumArtUrl = imgUrl }, + new Album { Title = "OK Computer", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Radiohead"], AlbumArtUrl = imgUrl }, + new Album { Title = "Olodum", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Olodum"], AlbumArtUrl = imgUrl }, + new Album { Title = "One Love", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["David Guetta"], AlbumArtUrl = imgUrl }, + new Album { Title = "Operation: Mindcrime", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Queensrÿche"], AlbumArtUrl = imgUrl }, + new Album { Title = "Opiate", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "Outbreak", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Dennis Chambers"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pachelbel: Canon & Gigue", Genre = genres["Classical"], Price = 8.99M, Artist = artists["English Concert & Trevor Pinnock"], AlbumArtUrl = imgUrl }, + new Album { Title = "Paid in Full", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Eric B. and Rakim"], AlbumArtUrl = imgUrl }, + new Album { Title = "Para Siempre", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Vicente Fernandez"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pause", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Four Tet"], AlbumArtUrl = imgUrl }, + new Album { Title = "Peace Sells... but Who's Buying", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Megadeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Physical Graffiti [Disc 1]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Physical Graffiti [Disc 2]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Physical Graffiti", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Piece Of Mind", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pinkerton", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Weezer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Plays Metallica By Four Cellos", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Apocalyptica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pop", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Powerslave", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prenda Minha", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Caetano Veloso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Presence", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Pretty Hate Machine", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Nine Inch Nails"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prisoner", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Jezabels"], AlbumArtUrl = imgUrl }, + new Album { Title = "Privateering", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Mark Knopfler"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prokofiev: Romeo & Juliet", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Michael Tilson Thomas"], AlbumArtUrl = imgUrl }, + new Album { Title = "Prokofiev: Symphony No.1", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sergei Prokofiev & Yuri Temirkanov"], AlbumArtUrl = imgUrl }, + new Album { Title = "PSY's Best 6th Part 1", Genre = genres["Pop"], Price = 8.99M, Artist = artists["PSY"], AlbumArtUrl = imgUrl }, + new Album { Title = "Purcell: The Fairy Queen", Genre = genres["Classical"], Price = 8.99M, Artist = artists["London Classical Players"], AlbumArtUrl = imgUrl }, + new Album { Title = "Purpendicular", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Purple", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Stone Temple Pilots"], AlbumArtUrl = imgUrl }, + new Album { Title = "Quanta Gente Veio Ver (Live)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Gilberto Gil"], AlbumArtUrl = imgUrl }, + new Album { Title = "Quanta Gente Veio ver--Bônus De Carnaval", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Gilberto Gil"], AlbumArtUrl = imgUrl }, + new Album { Title = "Quiet Songs", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Aisha Duo"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raices", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Los Tigres del Norte"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raising Hell", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Run DMC"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raoul and the Kings of Spain ", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tears For Fears"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rattle And Hum", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Raul Seixas", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Raul Seixas"], AlbumArtUrl = imgUrl }, + new Album { Title = "Recovery [Explicit]", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Eminem"], AlbumArtUrl = imgUrl }, + new Album { Title = "Reign In Blood", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Slayer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Relayed", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Yes"], AlbumArtUrl = imgUrl }, + new Album { Title = "ReLoad", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Respighi:Pines of Rome", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Eugene Ormandy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Restless and Wild", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Accept"], AlbumArtUrl = imgUrl }, + new Album { Title = "Retrospective I (1974-1980)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Rush"], AlbumArtUrl = imgUrl }, + new Album { Title = "Revelations", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Audioslave"], AlbumArtUrl = imgUrl }, + new Album { Title = "Revolver", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Beatles"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ride the Lighting ", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ride The Lightning", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ring My Bell", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Anita Ward"], AlbumArtUrl = imgUrl }, + new Album { Title = "Riot Act", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rise of the Phoenix", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Before the Dawn"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rock In Rio [CD1]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rock In Rio [CD2]", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rock In Rio [CD2]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Roda De Funk", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Funk Como Le Gusta"], AlbumArtUrl = imgUrl }, + new Album { Title = "Room for Squares", Genre = genres["Pop"], Price = 8.99M, Artist = artists["John Mayer"], AlbumArtUrl = imgUrl }, + new Album { Title = "Root Down", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Jimmy Smith"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rounds", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Four Tet"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rubber Factory", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Black Keys"], AlbumArtUrl = imgUrl }, + new Album { Title = "Rust in Peace", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Megadeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sambas De Enredo 2001", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Various Artists"], AlbumArtUrl = imgUrl }, + new Album { Title = "Santana - As Years Go By", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Santana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Santana Live", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Santana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Saturday Night Fever", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Bee Gees"], AlbumArtUrl = imgUrl }, + new Album { Title = "Scary Monsters and Nice Sprites", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Skrillex"], AlbumArtUrl = imgUrl }, + new Album { Title = "Scheherazade", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Chicago Symphony Orchestra & Fritz Reiner"], AlbumArtUrl = imgUrl }, + new Album { Title = "SCRIABIN: Vers la flamme", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Christopher O'Riley"], AlbumArtUrl = imgUrl }, + new Album { Title = "Second Coming", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Stone Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Serie Sem Limite (Disc 1)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Tim Maia"], AlbumArtUrl = imgUrl }, + new Album { Title = "Serie Sem Limite (Disc 2)", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Tim Maia"], AlbumArtUrl = imgUrl }, + new Album { Title = "Serious About Men", Genre = genres["Rap"], Price = 8.99M, Artist = artists["The Rubberbandits"], AlbumArtUrl = imgUrl }, + new Album { Title = "Seventh Son of a Seventh Son", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Short Bus", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Filter"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sibelius: Finlandia", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Berliner Philharmoniker"], AlbumArtUrl = imgUrl }, + new Album { Title = "Singles Collection", Genre = genres["Rock"], Price = 8.99M, Artist = artists["David Bowie"], AlbumArtUrl = imgUrl }, + new Album { Title = "Six Degrees of Inner Turbulence", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Dream Theater"], AlbumArtUrl = imgUrl }, + new Album { Title = "Slave To The Empire", Genre = genres["Metal"], Price = 8.99M, Artist = artists["T&N"], AlbumArtUrl = imgUrl }, + new Album { Title = "Slaves And Masters", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Slouching Towards Bethlehem", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Robert James"], AlbumArtUrl = imgUrl }, + new Album { Title = "Smash", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Offspring"], AlbumArtUrl = imgUrl }, + new Album { Title = "Something Special", Genre = genres["Country"], Price = 8.99M, Artist = artists["Dolly Parton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Somewhere in Time", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Song(s) You Know By Heart", Genre = genres["Country"], Price = 8.99M, Artist = artists["Jimmy Buffett"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sound of Music", Genre = genres["Punk"], Price = 8.99M, Artist = artists["Adicts"], AlbumArtUrl = imgUrl }, + new Album { Title = "South American Getaway", Genre = genres["Classical"], Price = 8.99M, Artist = artists["The 12 Cellists of The Berlin Philharmonic"], AlbumArtUrl = imgUrl }, + new Album { Title = "Sozinho Remix Ao Vivo", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Caetano Veloso"], AlbumArtUrl = imgUrl }, + new Album { Title = "Speak of the Devil", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Spiritual State", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Nujabes"], AlbumArtUrl = imgUrl }, + new Album { Title = "St. Anger", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Metallica"], AlbumArtUrl = imgUrl }, + new Album { Title = "Still Life", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Opeth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Stop Making Sense", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Talking Heads"], AlbumArtUrl = imgUrl }, + new Album { Title = "Stormbringer", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "Stranger than Fiction", Genre = genres["Punk"], Price = 8.99M, Artist = artists["Bad Religion"], AlbumArtUrl = imgUrl }, + new Album { Title = "Strauss: Waltzes", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Eugene Ormandy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Supermodified", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Amon Tobin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Supernatural", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Santana"], AlbumArtUrl = imgUrl }, + new Album { Title = "Surfing with the Alien (Remastered)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Joe Satriani"], AlbumArtUrl = imgUrl }, + new Album { Title = "Switched-On Bach", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Wendy Carlos"], AlbumArtUrl = imgUrl }, + new Album { Title = "Symphony", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Szymanowski: Piano Works, Vol. 1", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Martin Roscoe"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tchaikovsky: The Nutcracker", Genre = genres["Classical"], Price = 8.99M, Artist = artists["London Symphony Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ted Nugent", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Ted Nugent"], AlbumArtUrl = imgUrl }, + new Album { Title = "Teflon Don", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Rick Ross"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tell Another Joke at the Ol' Choppin' Block", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Danielson Famile"], AlbumArtUrl = imgUrl }, + new Album { Title = "Temple of the Dog", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Temple of the Dog"], AlbumArtUrl = imgUrl }, + new Album { Title = "Ten", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Texas Flood", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Stevie Ray Vaughan"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Battle Rages On", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Beast Live", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Paul D'Ianno"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best Of 1980-1990", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best of 1990–2000", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best of Beethoven", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Nicolaus Esterhazy Sinfonia"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best Of Billy Cobham", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Billy Cobham"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best of Ed Motta", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Ed Motta"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Best Of Van Halen, Vol. I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Bridge", Genre = genres["R&B"], Price = 8.99M, Artist = artists["Melanie Fiona"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Cage", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tygers of Pan Tang"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Chicago Transit Authority", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Chicago "], AlbumArtUrl = imgUrl }, + new Album { Title = "The Chronic", Genre = genres["Rap"], Price = 8.99M, Artist = artists["Dr. Dre"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Colour And The Shape", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Foo Fighters"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Crane Wife", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["The Decemberists"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Cream Of Clapton", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Eric Clapton"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Cure", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Cure"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Dark Side Of The Moon", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Divine Conspiracy", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Epica"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Doors", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Doors"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Dream of the Blue Turtles", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Sting"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Essential Miles Davis [Disc 1]", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Essential Miles Davis [Disc 2]", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Miles Davis"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Final Concerts (Disc 2)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deep Purple"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Final Frontier", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Head and the Heart", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Head and the Heart"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Joshua Tree", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Last Night of the Proms", Genre = genres["Classical"], Price = 8.99M, Artist = artists["BBC Concert Orchestra"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Lumineers", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Lumineers"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Number of The Beast", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Number of The Beast", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Police Greatest Hits", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Police"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Song Remains The Same (Disc 1)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Song Remains The Same (Disc 2)", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Southern Harmony and Musical Companion", Genre = genres["Blues"], Price = 8.99M, Artist = artists["The Black Crowes"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Spade", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Butch Walker & The Black Widows"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Stone Roses", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Stone Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Suburbs", Genre = genres["Indie"], Price = 8.99M, Artist = artists["Arcade Fire"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Three Tenors Disc1/Disc2", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Carreras, Pavarotti, Domingo"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Trees They Grow So High", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "The Wall", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "The X Factor", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Them Crooked Vultures", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Them Crooked Vultures"], AlbumArtUrl = imgUrl }, + new Album { Title = "This Is Happening", Genre = genres["Rock"], Price = 8.99M, Artist = artists["LCD Soundsystem"], AlbumArtUrl = imgUrl }, + new Album { Title = "Thunder, Lightning, Strike", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Go! Team"], AlbumArtUrl = imgUrl }, + new Album { Title = "Time to Say Goodbye", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sarah Brightman"], AlbumArtUrl = imgUrl }, + new Album { Title = "Time, Love & Tenderness", Genre = genres["Pop"], Price = 8.99M, Artist = artists["Michael Bolton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tomorrow Starts Today", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Mobile"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tribute", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Ozzy Osbourne"], AlbumArtUrl = imgUrl }, + new Album { Title = "Tuesday Night Music Club", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Sheryl Crow"], AlbumArtUrl = imgUrl }, + new Album { Title = "Umoja", Genre = genres["Rock"], Price = 8.99M, Artist = artists["BLØF"], AlbumArtUrl = imgUrl }, + new Album { Title = "Under the Pink", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Tori Amos"], AlbumArtUrl = imgUrl }, + new Album { Title = "Undertow", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Tool"], AlbumArtUrl = imgUrl }, + new Album { Title = "Un-Led-Ed", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Dread Zeppelin"], AlbumArtUrl = imgUrl }, + new Album { Title = "Unplugged [Live]", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Kiss"], AlbumArtUrl = imgUrl }, + new Album { Title = "Unplugged", Genre = genres["Blues"], Price = 8.99M, Artist = artists["Eric Clapton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Unplugged", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Eric Clapton"], AlbumArtUrl = imgUrl }, + new Album { Title = "Untrue", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Burial"], AlbumArtUrl = imgUrl }, + new Album { Title = "Use Your Illusion I", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Use Your Illusion II", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Use Your Illusion II", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Guns N' Roses"], AlbumArtUrl = imgUrl }, + new Album { Title = "Van Halen III", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Van Halen", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Van Halen"], AlbumArtUrl = imgUrl }, + new Album { Title = "Version 2.0", Genre = genres["Alternative"], Price = 8.99M, Artist = artists["Garbage"], AlbumArtUrl = imgUrl }, + new Album { Title = "Vinicius De Moraes", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Vinícius De Moraes"], AlbumArtUrl = imgUrl }, + new Album { Title = "Virtual XI", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Iron Maiden"], AlbumArtUrl = imgUrl }, + new Album { Title = "Voodoo Lounge", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Rolling Stones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Vozes do MPB", Genre = genres["Latin"], Price = 8.99M, Artist = artists["Various Artists"], AlbumArtUrl = imgUrl }, + new Album { Title = "Vs.", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pearl Jam"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wagner: Favourite Overtures", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Sir Georg Solti & Wiener Philharmoniker"], AlbumArtUrl = imgUrl }, + new Album { Title = "Walking Into Clarksdale", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Page & Plant"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wapi Yo", Genre = genres["World"], Price = 8.99M, Artist = artists["Lokua Kanza"], AlbumArtUrl = imgUrl }, + new Album { Title = "War", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Warner 25 Anos", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Antônio Carlos Jobim"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wasteland R&Btheque", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Raunchy"], AlbumArtUrl = imgUrl }, + new Album { Title = "Watermark", Genre = genres["Electronic"], Price = 8.99M, Artist = artists["Enya"], AlbumArtUrl = imgUrl }, + new Album { Title = "We Were Exploding Anyway", Genre = genres["Rock"], Price = 8.99M, Artist = artists["65daysofstatic"], AlbumArtUrl = imgUrl }, + new Album { Title = "Weill: The Seven Deadly Sins", Genre = genres["Classical"], Price = 8.99M, Artist = artists["Orchestre de l'Opéra de Lyon"], AlbumArtUrl = imgUrl }, + new Album { Title = "White Pony", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Deftones"], AlbumArtUrl = imgUrl }, + new Album { Title = "Who's Next", Genre = genres["Rock"], Price = 8.99M, Artist = artists["The Who"], AlbumArtUrl = imgUrl }, + new Album { Title = "Wish You Were Here", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Pink Floyd"], AlbumArtUrl = imgUrl }, + new Album { Title = "With Oden on Our Side", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Amon Amarth"], AlbumArtUrl = imgUrl }, + new Album { Title = "Worlds", Genre = genres["Jazz"], Price = 8.99M, Artist = artists["Aaron Goldberg"], AlbumArtUrl = imgUrl }, + new Album { Title = "Worship Music", Genre = genres["Metal"], Price = 8.99M, Artist = artists["Anthrax"], AlbumArtUrl = imgUrl }, + new Album { Title = "X&Y", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Coldplay"], AlbumArtUrl = imgUrl }, + new Album { Title = "Xinti", Genre = genres["World"], Price = 8.99M, Artist = artists["Sara Tavares"], AlbumArtUrl = imgUrl }, + new Album { Title = "Yano", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Yano"], AlbumArtUrl = imgUrl }, + new Album { Title = "Yesterday Once More Disc 1/Disc 2", Genre = genres["Pop"], Price = 8.99M, Artist = artists["The Carpenters"], AlbumArtUrl = imgUrl }, + new Album { Title = "Zooropa", Genre = genres["Rock"], Price = 8.99M, Artist = artists["U2"], AlbumArtUrl = imgUrl }, + new Album { Title = "Zoso", Genre = genres["Rock"], Price = 8.99M, Artist = artists["Led Zeppelin"], AlbumArtUrl = imgUrl }, + }; + + foreach (var album in albums) + { + album.ArtistId = album.Artist.ArtistId; + album.GenreId = album.Genre.GenreId; + } + + return albums; + } + + private static Dictionary artists; + public static Dictionary Artists + { + get + { + if (artists == null) + { + var artistsList = new Artist[] + { + new Artist { Name = "65daysofstatic" }, + new Artist { Name = "Aaron Goldberg" }, + new Artist { Name = "Above & Beyond" }, + new Artist { Name = "Above the Fold" }, + new Artist { Name = "AC/DC" }, + new Artist { Name = "Accept" }, + new Artist { Name = "Adicts" }, + new Artist { Name = "Adrian Leaper & Doreen de Feis" }, + new Artist { Name = "Aerosmith" }, + new Artist { Name = "Aisha Duo" }, + new Artist { Name = "Al di Meola" }, + new Artist { Name = "Alabama Shakes" }, + new Artist { Name = "Alanis Morissette" }, + new Artist { Name = "Alberto Turco & Nova Schola Gregoriana" }, + new Artist { Name = "Alice in Chains" }, + new Artist { Name = "Alison Krauss" }, + new Artist { Name = "Amon Amarth" }, + new Artist { Name = "Amon Tobin" }, + new Artist { Name = "Amr Diab" }, + new Artist { Name = "Amy Winehouse" }, + new Artist { Name = "Anita Ward" }, + new Artist { Name = "Anthrax" }, + new Artist { Name = "Antônio Carlos Jobim" }, + new Artist { Name = "Apocalyptica" }, + new Artist { Name = "Aqua" }, + new Artist { Name = "Armand Van Helden" }, + new Artist { Name = "Arcade Fire" }, + new Artist { Name = "Audioslave" }, + new Artist { Name = "Bad Religion" }, + new Artist { Name = "Barenaked Ladies" }, + new Artist { Name = "BBC Concert Orchestra" }, + new Artist { Name = "Bee Gees" }, + new Artist { Name = "Before the Dawn" }, + new Artist { Name = "Berliner Philharmoniker" }, + new Artist { Name = "Billy Cobham" }, + new Artist { Name = "Black Label Society" }, + new Artist { Name = "Black Sabbath" }, + new Artist { Name = "BLØF" }, + new Artist { Name = "Blues Traveler" }, + new Artist { Name = "Boston Symphony Orchestra & Seiji Ozawa" }, + new Artist { Name = "Britten Sinfonia, Ivor Bolton & Lesley Garrett" }, + new Artist { Name = "Bruce Dickinson" }, + new Artist { Name = "Buddy Guy" }, + new Artist { Name = "Burial" }, + new Artist { Name = "Butch Walker & The Black Widows" }, + new Artist { Name = "Caetano Veloso" }, + new Artist { Name = "Cake" }, + new Artist { Name = "Calexico" }, + new Artist { Name = "Carly Rae Jepsen" }, + new Artist { Name = "Carreras, Pavarotti, Domingo" }, + new Artist { Name = "Cássia Eller" }, + new Artist { Name = "Cayouche" }, + new Artist { Name = "Chic" }, + new Artist { Name = "Chicago " }, + new Artist { Name = "Chicago Symphony Orchestra & Fritz Reiner" }, + new Artist { Name = "Chico Buarque" }, + new Artist { Name = "Chico Science & Nação Zumbi" }, + new Artist { Name = "Choir Of Westminster Abbey & Simon Preston" }, + new Artist { Name = "Chris Cornell" }, + new Artist { Name = "Christopher O'Riley" }, + new Artist { Name = "Cidade Negra" }, + new Artist { Name = "Cláudio Zoli" }, + new Artist { Name = "Coldplay" }, + new Artist { Name = "Creedence Clearwater Revival" }, + new Artist { Name = "Crosby, Stills, Nash, and Young" }, + new Artist { Name = "Daft Punk" }, + new Artist { Name = "Danielson Famile" }, + new Artist { Name = "David Bowie" }, + new Artist { Name = "David Coverdale" }, + new Artist { Name = "David Guetta" }, + new Artist { Name = "deadmau5" }, + new Artist { Name = "Deep Purple" }, + new Artist { Name = "Def Leppard" }, + new Artist { Name = "Deftones" }, + new Artist { Name = "Dennis Chambers" }, + new Artist { Name = "Deva Premal" }, + new Artist { Name = "Dio" }, + new Artist { Name = "Djavan" }, + new Artist { Name = "Dolly Parton" }, + new Artist { Name = "Donna Summer" }, + new Artist { Name = "Dr. Dre" }, + new Artist { Name = "Dread Zeppelin" }, + new Artist { Name = "Dream Theater" }, + new Artist { Name = "Duck Sauce" }, + new Artist { Name = "Earl Scruggs" }, + new Artist { Name = "Ed Motta" }, + new Artist { Name = "Edo de Waart & San Francisco Symphony" }, + new Artist { Name = "Elis Regina" }, + new Artist { Name = "Eminem" }, + new Artist { Name = "English Concert & Trevor Pinnock" }, + new Artist { Name = "Enya" }, + new Artist { Name = "Epica" }, + new Artist { Name = "Eric B. and Rakim" }, + new Artist { Name = "Eric Clapton" }, + new Artist { Name = "Eugene Ormandy" }, + new Artist { Name = "Faith No More" }, + new Artist { Name = "Falamansa" }, + new Artist { Name = "Filter" }, + new Artist { Name = "Foo Fighters" }, + new Artist { Name = "Four Tet" }, + new Artist { Name = "Frank Zappa & Captain Beefheart" }, + new Artist { Name = "Fretwork" }, + new Artist { Name = "Funk Como Le Gusta" }, + new Artist { Name = "Garbage" }, + new Artist { Name = "Gerald Moore" }, + new Artist { Name = "Gilberto Gil" }, + new Artist { Name = "Godsmack" }, + new Artist { Name = "Gonzaguinha" }, + new Artist { Name = "Göteborgs Symfoniker & Neeme Järvi" }, + new Artist { Name = "Guns N' Roses" }, + new Artist { Name = "Gustav Mahler" }, + new Artist { Name = "In This Moment" }, + new Artist { Name = "Incognito" }, + new Artist { Name = "INXS" }, + new Artist { Name = "Iron Maiden" }, + new Artist { Name = "Jagjit Singh" }, + new Artist { Name = "James Levine" }, + new Artist { Name = "Jamiroquai" }, + new Artist { Name = "Jimi Hendrix" }, + new Artist { Name = "Jimmy Buffett" }, + new Artist { Name = "Jimmy Smith" }, + new Artist { Name = "Joe Satriani" }, + new Artist { Name = "John Digweed" }, + new Artist { Name = "John Mayer" }, + new Artist { Name = "Jorge Ben" }, + new Artist { Name = "Jota Quest" }, + new Artist { Name = "Journey" }, + new Artist { Name = "Judas Priest" }, + new Artist { Name = "Julian Bream" }, + new Artist { Name = "Justice" }, + new Artist { Name = "Orchestre de l'Opéra de Lyon" }, + new Artist { Name = "King Crimson" }, + new Artist { Name = "Kiss" }, + new Artist { Name = "LCD Soundsystem" }, + new Artist { Name = "Le Tigre" }, + new Artist { Name = "Led Zeppelin" }, + new Artist { Name = "Legião Urbana" }, + new Artist { Name = "Lenny Kravitz" }, + new Artist { Name = "Les Arts Florissants & William Christie" }, + new Artist { Name = "Limp Bizkit" }, + new Artist { Name = "Linkin Park" }, + new Artist { Name = "Live" }, + new Artist { Name = "Lokua Kanza" }, + new Artist { Name = "London Symphony Orchestra" }, + new Artist { Name = "Los Tigres del Norte" }, + new Artist { Name = "Luciana Souza/Romero Lubambo" }, + new Artist { Name = "Lulu Santos" }, + new Artist { Name = "Lura" }, + new Artist { Name = "Marcos Valle" }, + new Artist { Name = "Marillion" }, + new Artist { Name = "Marisa Monte" }, + new Artist { Name = "Mark Knopfler" }, + new Artist { Name = "Martin Roscoe" }, + new Artist { Name = "Massive Attack" }, + new Artist { Name = "Maurizio Pollini" }, + new Artist { Name = "Megadeth" }, + new Artist { Name = "Mela Tenenbaum, Pro Musica Prague & Richard Kapp" }, + new Artist { Name = "Melanie Fiona" }, + new Artist { Name = "Men At Work" }, + new Artist { Name = "Metallica" }, + new Artist { Name = "M-Flo" }, + new Artist { Name = "Michael Bolton" }, + new Artist { Name = "Michael Tilson Thomas" }, + new Artist { Name = "Miles Davis" }, + new Artist { Name = "Milton Nascimento" }, + new Artist { Name = "Mobile" }, + new Artist { Name = "Modest Mouse" }, + new Artist { Name = "Mötley Crüe" }, + new Artist { Name = "Motörhead" }, + new Artist { Name = "Mumford & Sons" }, + new Artist { Name = "Munkle" }, + new Artist { Name = "Nash Ensemble" }, + new Artist { Name = "Neil Young" }, + new Artist { Name = "New York Dolls" }, + new Artist { Name = "Nick Cave and the Bad Seeds" }, + new Artist { Name = "Nicolaus Esterhazy Sinfonia" }, + new Artist { Name = "Nine Inch Nails" }, + new Artist { Name = "Nirvana" }, + new Artist { Name = "Norah Jones" }, + new Artist { Name = "Nujabes" }, + new Artist { Name = "O Terço" }, + new Artist { Name = "Oasis" }, + new Artist { Name = "Olodum" }, + new Artist { Name = "Opeth" }, + new Artist { Name = "Orchestra of The Age of Enlightenment" }, + new Artist { Name = "Os Paralamas Do Sucesso" }, + new Artist { Name = "Ozzy Osbourne" }, + new Artist { Name = "Paddy Casey" }, + new Artist { Name = "Page & Plant" }, + new Artist { Name = "Papa Wemba" }, + new Artist { Name = "Paul D'Ianno" }, + new Artist { Name = "Paul Oakenfold" }, + new Artist { Name = "Paul Van Dyk" }, + new Artist { Name = "Pearl Jam" }, + new Artist { Name = "Pet Shop Boys" }, + new Artist { Name = "Pink Floyd" }, + new Artist { Name = "Plug" }, + new Artist { Name = "Porcupine Tree" }, + new Artist { Name = "Portishead" }, + new Artist { Name = "Prince" }, + new Artist { Name = "Projected" }, + new Artist { Name = "PSY" }, + new Artist { Name = "Public Enemy" }, + new Artist { Name = "Queen" }, + new Artist { Name = "Queensrÿche" }, + new Artist { Name = "R.E.M." }, + new Artist { Name = "Radiohead" }, + new Artist { Name = "Rancid" }, + new Artist { Name = "Raul Seixas" }, + new Artist { Name = "Raunchy" }, + new Artist { Name = "Red Hot Chili Peppers" }, + new Artist { Name = "Rick Ross" }, + new Artist { Name = "Robert James" }, + new Artist { Name = "London Classical Players" }, + new Artist { Name = "Royal Philharmonic Orchestra" }, + new Artist { Name = "Run DMC" }, + new Artist { Name = "Rush" }, + new Artist { Name = "Santana" }, + new Artist { Name = "Sara Tavares" }, + new Artist { Name = "Sarah Brightman" }, + new Artist { Name = "Sasha" }, + new Artist { Name = "Scholars Baroque Ensemble" }, + new Artist { Name = "Scorpions" }, + new Artist { Name = "Sergei Prokofiev & Yuri Temirkanov" }, + new Artist { Name = "Sheryl Crow" }, + new Artist { Name = "Sir Georg Solti & Wiener Philharmoniker" }, + new Artist { Name = "Skank" }, + new Artist { Name = "Skrillex" }, + new Artist { Name = "Slash" }, + new Artist { Name = "Slayer" }, + new Artist { Name = "Soul-Junk" }, + new Artist { Name = "Soundgarden" }, + new Artist { Name = "Spyro Gyra" }, + new Artist { Name = "Stevie Ray Vaughan & Double Trouble" }, + new Artist { Name = "Stevie Ray Vaughan" }, + new Artist { Name = "Sting" }, + new Artist { Name = "Stone Temple Pilots" }, + new Artist { Name = "Styx" }, + new Artist { Name = "Sufjan Stevens" }, + new Artist { Name = "Supreme Beings of Leisure" }, + new Artist { Name = "System Of A Down" }, + new Artist { Name = "T&N" }, + new Artist { Name = "Talking Heads" }, + new Artist { Name = "Tears For Fears" }, + new Artist { Name = "Ted Nugent" }, + new Artist { Name = "Temple of the Dog" }, + new Artist { Name = "Terry Bozzio, Tony Levin & Steve Stevens" }, + new Artist { Name = "The 12 Cellists of The Berlin Philharmonic" }, + new Artist { Name = "The Axis of Awesome" }, + new Artist { Name = "The Beatles" }, + new Artist { Name = "The Black Crowes" }, + new Artist { Name = "The Black Keys" }, + new Artist { Name = "The Carpenters" }, + new Artist { Name = "The Cat Empire" }, + new Artist { Name = "The Cult" }, + new Artist { Name = "The Cure" }, + new Artist { Name = "The Decemberists" }, + new Artist { Name = "The Doors" }, + new Artist { Name = "The Eagles of Death Metal" }, + new Artist { Name = "The Go! Team" }, + new Artist { Name = "The Head and the Heart" }, + new Artist { Name = "The Jezabels" }, + new Artist { Name = "The King's Singers" }, + new Artist { Name = "The Lumineers" }, + new Artist { Name = "The Offspring" }, + new Artist { Name = "The Police" }, + new Artist { Name = "The Posies" }, + new Artist { Name = "The Prodigy" }, + new Artist { Name = "The Rolling Stones" }, + new Artist { Name = "The Rubberbandits" }, + new Artist { Name = "The Smashing Pumpkins" }, + new Artist { Name = "The Stone Roses" }, + new Artist { Name = "The Who" }, + new Artist { Name = "Them Crooked Vultures" }, + new Artist { Name = "TheStart" }, + new Artist { Name = "Thievery Corporation" }, + new Artist { Name = "Tiësto" }, + new Artist { Name = "Tim Maia" }, + new Artist { Name = "Ton Koopman" }, + new Artist { Name = "Tool" }, + new Artist { Name = "Tori Amos" }, + new Artist { Name = "Trampled By Turtles" }, + new Artist { Name = "Trans-Siberian Orchestra" }, + new Artist { Name = "Tygers of Pan Tang" }, + new Artist { Name = "U2" }, + new Artist { Name = "UB40" }, + new Artist { Name = "Uh Huh Her " }, + new Artist { Name = "Van Halen" }, + new Artist { Name = "Various Artists" }, + new Artist { Name = "Velvet Revolver" }, + new Artist { Name = "Venus Hum" }, + new Artist { Name = "Vicente Fernandez" }, + new Artist { Name = "Vinícius De Moraes" }, + new Artist { Name = "Weezer" }, + new Artist { Name = "Weird Al" }, + new Artist { Name = "Wendy Carlos" }, + new Artist { Name = "Wilhelm Kempff" }, + new Artist { Name = "Yano" }, + new Artist { Name = "Yehudi Menuhin" }, + new Artist { Name = "Yes" }, + new Artist { Name = "Yo-Yo Ma" }, + new Artist { Name = "Zeca Pagodinho" }, + new Artist { Name = "אריק אינשטיין"} + }; + + // TODO [EF] Swap to store generated keys when available + int artistId = 1; + artists = new Dictionary(); + foreach (Artist artist in artistsList) + { + artist.ArtistId = artistId++; + artists.Add(artist.Name, artist); + } + } + + return artists; + } + } + + private static Dictionary genres; + public static Dictionary Genres + { + get + { + if (genres == null) + { + var genresList = new Genre[] + { + new Genre { Name = "Pop" }, + new Genre { Name = "Rock" }, + new Genre { Name = "Jazz" }, + new Genre { Name = "Metal" }, + new Genre { Name = "Electronic" }, + new Genre { Name = "Blues" }, + new Genre { Name = "Latin" }, + new Genre { Name = "Rap" }, + new Genre { Name = "Classical" }, + new Genre { Name = "Alternative" }, + new Genre { Name = "Country" }, + new Genre { Name = "R&B" }, + new Genre { Name = "Indie" }, + new Genre { Name = "Punk" }, + new Genre { Name = "World" } + }; + + genres = new Dictionary(); + // TODO [EF] Swap to store generated keys when available + int genreId = 1; + foreach (Genre genre in genresList) + { + genre.GenreId = genreId++; + + // TODO [EF] Remove when null values are supported by update pipeline + genre.Description = genre.Name + " is great music (if you like it)."; + + genres.Add(genre.Name, genre); + } + } + + return genres; + } + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Apis/Models/ShoppingCart.cs b/samples/angular/MusicStore/Apis/Models/ShoppingCart.cs new file mode 100644 index 000000000000..41b327746f3d --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/ShoppingCart.cs @@ -0,0 +1,207 @@ +using Microsoft.AspNet.Http; +using Microsoft.Data.Entity; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MusicStore.Models +{ + public partial class ShoppingCart + { + MusicStoreContext _db; + string ShoppingCartId { get; set; } + + public ShoppingCart(MusicStoreContext db) + { + _db = db; + } + + public static ShoppingCart GetCart(MusicStoreContext db, HttpContext context) + { + var cart = new ShoppingCart(db); + cart.ShoppingCartId = cart.GetCartId(context); + return cart; + } + + public void AddToCart(Album album) + { + // Get the matching cart and album instances + var cartItem = _db.CartItems.SingleOrDefault( + c => c.CartId == ShoppingCartId + && c.AlbumId == album.AlbumId); + + if (cartItem == null) + { + // TODO [EF] Swap to store generated key once we support identity pattern + var nextCartItemId = _db.CartItems.Any() + ? _db.CartItems.Max(c => c.CartItemId) + 1 + : 1; + + // Create a new cart item if no cart item exists + cartItem = new CartItem + { + CartItemId = nextCartItemId, + AlbumId = album.AlbumId, + CartId = ShoppingCartId, + Count = 1, + DateCreated = DateTime.Now + }; + + _db.CartItems.Add(cartItem); + } + else + { + // If the item does exist in the cart, then add one to the quantity + cartItem.Count++; + + // TODO [EF] Remove this line once change detection is available + _db.Update(cartItem); + } + } + + public int RemoveFromCart(int id) + { + // Get the cart + var cartItem = _db.CartItems.Single( + cart => cart.CartId == ShoppingCartId + && cart.CartItemId == id); + + int itemCount = 0; + + if (cartItem != null) + { + if (cartItem.Count > 1) + { + cartItem.Count--; + + // TODO [EF] Remove this line once change detection is available + _db.Update(cartItem); + + itemCount = cartItem.Count; + } + else + { + _db.CartItems.Remove(cartItem); + } + } + + return itemCount; + } + + public void EmptyCart() + { + var cartItems = _db.CartItems.Where(cart => cart.CartId == ShoppingCartId); + + foreach (var cartItem in cartItems) + { + _db.Remove(cartItem); + } + } + + public List GetCartItems() + { + var cartItems = _db.CartItems.Where(cart => cart.CartId == ShoppingCartId).ToList(); + //TODO: Auto population of the related album data not available until EF feature is lighted up. + foreach (var cartItem in cartItems) + { + cartItem.Album = _db.Albums.Single(a => a.AlbumId == cartItem.AlbumId); + } + + return cartItems; + } + + public int GetCount() + { + // Get the count of each item in the cart and sum them up + int? count = (from cartItems in _db.CartItems + where cartItems.CartId == ShoppingCartId + select (int?)cartItems.Count).Sum(); + + // Return 0 if all entries are null + return count ?? 0; + } + + public decimal GetTotal() + { + // Multiply album price by count of that album to get + // the current price for each of those albums in the cart + // sum all album price totals to get the cart total + + // TODO Collapse to a single query once EF supports querying related data + decimal total = 0; + foreach (var item in _db.CartItems.Where(c => c.CartId == ShoppingCartId)) + { + var album = _db.Albums.Single(a => a.AlbumId == item.AlbumId); + total += item.Count * album.Price; + } + + return total; + } + + public int CreateOrder(Order order) + { + decimal orderTotal = 0; + + var cartItems = GetCartItems(); + + // TODO [EF] Swap to store generated identity key when supported + var nextId = _db.OrderDetails.Any() + ? _db.OrderDetails.Max(o => o.OrderDetailId) + 1 + : 1; + + // Iterate over the items in the cart, adding the order details for each + foreach (var item in cartItems) + { + //var album = _db.Albums.Find(item.AlbumId); + var album = _db.Albums.Single(a => a.AlbumId == item.AlbumId); + + var orderDetail = new OrderDetail + { + OrderDetailId = nextId, + AlbumId = item.AlbumId, + OrderId = order.OrderId, + UnitPrice = album.Price, + Quantity = item.Count, + }; + + // Set the order total of the shopping cart + orderTotal += (item.Count * album.Price); + + _db.OrderDetails.Add(orderDetail); + + nextId++; + } + + // Set the order's total to the orderTotal count + order.Total = orderTotal; + + // Empty the shopping cart + EmptyCart(); + + // Return the OrderId as the confirmation number + return order.OrderId; + } + + // We're using HttpContextBase to allow access to cookies. + public string GetCartId(HttpContext context) + { + var sessionCookie = context.Request.Cookies["Session"]; + string cartId = null; + + if (string.IsNullOrWhiteSpace(sessionCookie)) + { + //A GUID to hold the cartId. + cartId = Guid.NewGuid().ToString(); + + // Send cart Id as a cookie to the client. + context.Response.Cookies.Append("Session", cartId); + } + else + { + cartId = sessionCookie; + } + + return cartId; + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Controllers/HomeController.cs b/samples/angular/MusicStore/Controllers/HomeController.cs new file mode 100755 index 000000000000..aabaed6bf4e5 --- /dev/null +++ b/samples/angular/MusicStore/Controllers/HomeController.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Mvc; + +namespace MusicStore.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + var url = Request.Path.Value; + if (url.EndsWith(".ico") || url.EndsWith(".map")) { + return new HttpStatusCodeResult(404); + } else { + return View(); + } + } + + public IActionResult Error() + { + return View("~/Views/Shared/Error.cshtml"); + } + } +} diff --git a/samples/angular/MusicStore/Infrastructure/ApiResult.cs b/samples/angular/MusicStore/Infrastructure/ApiResult.cs new file mode 100644 index 000000000000..9aae7804e69b --- /dev/null +++ b/samples/angular/MusicStore/Infrastructure/ApiResult.cs @@ -0,0 +1,63 @@ +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MusicStore.Infrastructure +{ + public class ApiResult : ActionResult + { + public ApiResult(ModelStateDictionary modelState) + : this() + { + if (modelState.Any(m => m.Value.Errors.Count > 0)) + { + StatusCode = 400; + Message = "The model submitted was invalid. Please correct the specified errors and try again."; + ModelErrors = modelState + .SelectMany(m => m.Value.Errors.Select(me => new ModelError + { + FieldName = m.Key, + ErrorMessage = me.ErrorMessage + })); + } + } + + public ApiResult() + { + + } + + [JsonIgnore] + public int? StatusCode { get; set; } + + public string Message { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public object Data { get; set; } + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public IEnumerable ModelErrors { get; set; } + + public override Task ExecuteResultAsync(ActionContext context) + { + if (StatusCode.HasValue) + { + context.HttpContext.Response.StatusCode = StatusCode.Value; + } + + var json = new JsonResult(this); + return json.ExecuteResultAsync(context); + } + + public class ModelError + { + public string FieldName { get; set; } + + public string ErrorMessage { get; set; } + } + } +} diff --git a/samples/angular/MusicStore/Infrastructure/NoCacheAttribute.cs b/samples/angular/MusicStore/Infrastructure/NoCacheAttribute.cs new file mode 100644 index 000000000000..7c0a9cd72f0d --- /dev/null +++ b/samples/angular/MusicStore/Infrastructure/NoCacheAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNet.Mvc; +using System; +using Microsoft.AspNet.Mvc.Filters; + +namespace MusicStore.Infrastructure +{ + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public sealed class NoCacheAttribute : ActionFilterAttribute + { + public override void OnResultExecuting(ResultExecutingContext context) + { + context.HttpContext.Response.Headers["Cache-Control"] = "no-cache, no-store, max-age=0"; + context.HttpContext.Response.Headers["Pragma"] = "no-cache"; + context.HttpContext.Response.Headers["Expires"] = "-1"; + + base.OnResultExecuting(context); + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Infrastructure/PagedList.cs b/samples/angular/MusicStore/Infrastructure/PagedList.cs new file mode 100644 index 000000000000..98c5b139c9da --- /dev/null +++ b/samples/angular/MusicStore/Infrastructure/PagedList.cs @@ -0,0 +1,150 @@ +using Microsoft.Data.Entity; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace MusicStore.Infrastructure +{ + public interface IPagedList + { + IEnumerable Data { get; } + + int Page { get; } + + int PageSize { get; } + + int TotalCount { get; } + } + + internal class PagedList : IPagedList + { + public PagedList(IEnumerable data, int page, int pageSize, int totalCount) + { + Data = data; + Page = page; + PageSize = pageSize; + TotalCount = totalCount; + } + + public IEnumerable Data { get; private set; } + + public int Page { get; private set; } + + public int PageSize { get; private set; } + + public int TotalCount { get; private set; } + } + + public static class PagedListExtensions + { + public static IPagedList ToPagedList(this IQueryable query, int page, int pageSize) + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var pagingConfig = new PagingConfig(page, pageSize); + var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); + + var data = query + .Skip(skipCount) + .Take(pagingConfig.PageSize) + .ToList(); + + if (skipCount > 0 && data.Count == 0) + { + // Requested page has no records, just return the first page + pagingConfig.Page = 1; + data = query + .Take(pagingConfig.PageSize) + .ToList(); + } + + return new PagedList(data, pagingConfig.Page, pagingConfig.PageSize, query.Count()); + } + + public static Task> ToPagedListAsync(this IQueryable query, int page, int pageSize, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending) + where TModel : class + { + return ToPagedListAsync(query, page, pageSize, sortExpression, defaultSortExpression, defaultSortDirection, null); + } + + public static async Task> ToPagedListAsync(this IQueryable query, int page, int pageSize, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection, Func selector) + where TModel : class + where TResult : class + { + if (query == null) + { + throw new ArgumentNullException("query"); + } + + var pagingConfig = new PagingConfig(page, pageSize); + var skipCount = ValidatePagePropertiesAndGetSkipCount(pagingConfig); + var dataQuery = query; + + if (defaultSortExpression != null) + { + dataQuery = dataQuery + .SortBy(sortExpression, defaultSortExpression); + } + + var data = await dataQuery + .Skip(skipCount) + .Take(pagingConfig.PageSize) + .ToListAsync(); + + if (skipCount > 0 && data.Count == 0) + { + // Requested page has no records, just return the first page + pagingConfig.Page = 1; + data = await dataQuery + .Take(pagingConfig.PageSize) + .ToListAsync(); + } + + var count = await query.CountAsync(); + + var resultData = selector != null + ? data.Select(selector) + : data.Cast(); + + return new PagedList(resultData, pagingConfig.Page, pagingConfig.PageSize, count); + } + + private static int ValidatePagePropertiesAndGetSkipCount(PagingConfig pagingConfig) + { + if (pagingConfig.Page < 1) + { + pagingConfig.Page = 1; + } + + if (pagingConfig.PageSize < 10) + { + pagingConfig.PageSize = 10; + } + + if (pagingConfig.PageSize > 100) + { + pagingConfig.PageSize = 100; + } + + return pagingConfig.PageSize * (pagingConfig.Page - 1); + } + + internal class PagingConfig + { + public PagingConfig(int page, int pageSize) + { + Page = page; + PageSize = pageSize; + } + + public int Page { get; set; } + + public int PageSize { get; set; } + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Infrastructure/SortDirection.cs b/samples/angular/MusicStore/Infrastructure/SortDirection.cs new file mode 100644 index 000000000000..28f7e86c0409 --- /dev/null +++ b/samples/angular/MusicStore/Infrastructure/SortDirection.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MusicStore.Infrastructure +{ + public enum SortDirection + { + Ascending, + Descending + } +} diff --git a/samples/angular/MusicStore/Infrastructure/SortExpression.cs b/samples/angular/MusicStore/Infrastructure/SortExpression.cs new file mode 100644 index 000000000000..279efb71c4e5 --- /dev/null +++ b/samples/angular/MusicStore/Infrastructure/SortExpression.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc.ViewFeatures; + +namespace MusicStore.Infrastructure +{ + public static class SortExpression + { + private const string SORT_DIRECTION_DESC = " DESC"; + + public static IQueryable SortBy(this IQueryable query, string sortExpression, Expression> defaultSortExpression, SortDirection defaultSortDirection = SortDirection.Ascending) where TModel : class + { + return SortBy(query, sortExpression ?? Create(defaultSortExpression, defaultSortDirection)); + } + + public static string Create(Expression> expression, SortDirection sortDirection = SortDirection.Ascending) where TModel : class + { + var expressionText = ExpressionHelper.GetExpressionText(expression); + // TODO: Validate the expression depth, etc. + + var sortExpression = expressionText; + + if (sortDirection == SortDirection.Descending) + { + sortExpression += SORT_DIRECTION_DESC; + } + + return sortExpression; + } + + public static IQueryable SortBy(this IQueryable source, string sortExpression) where T : class + { + if (source == null) + { + throw new ArgumentNullException("source"); + } + + if (String.IsNullOrWhiteSpace(sortExpression)) + { + return source; + } + + sortExpression = sortExpression.Trim(); + var isDescending = false; + + // DataSource control passes the sort parameter with a direction + // if the direction is descending + if (sortExpression.EndsWith(SORT_DIRECTION_DESC, StringComparison.OrdinalIgnoreCase)) + { + isDescending = true; + var descIndex = sortExpression.Length - SORT_DIRECTION_DESC.Length; + sortExpression = sortExpression.Substring(0, descIndex).Trim(); + } + + if (string.IsNullOrEmpty(sortExpression)) + { + return source; + } + + ParameterExpression parameter = Expression.Parameter(source.ElementType, String.Empty); + + // Build up the property expression, e.g.: (m => m.Foo.Bar) + var sortExpressionParts = sortExpression.Split('.'); + Expression propertyExpression = parameter; + foreach (var property in sortExpressionParts) + { + propertyExpression = Expression.Property(propertyExpression, property); + } + + LambdaExpression lambda = Expression.Lambda(propertyExpression, parameter); + + var methodName = (isDescending) ? "OrderByDescending" : "OrderBy"; + + Expression methodCallExpression = Expression.Call( + typeof(Queryable), + methodName, + new[] { source.ElementType, propertyExpression.Type }, + source.Expression, + Expression.Quote(lambda)); + + return (IQueryable)source.Provider.CreateQuery(methodCallExpression); + } + } +} diff --git a/samples/angular/MusicStore/SiteSettings.cs b/samples/angular/MusicStore/SiteSettings.cs new file mode 100644 index 000000000000..50a86c2451ef --- /dev/null +++ b/samples/angular/MusicStore/SiteSettings.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MusicStore +{ + public class SiteSettings + { + public string DefaultAdminUsername { get; set; } + public string DefaultAdminPassword { get; set; } + } +} diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs new file mode 100755 index 000000000000..89fb223e0ffd --- /dev/null +++ b/samples/angular/MusicStore/Startup.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.AspNet.Authorization; +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.AspNet.NodeServices; +using Microsoft.Data.Entity; +using Microsoft.Dnx.Runtime; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; +using MusicStore.Apis; +using MusicStore.Models; + +namespace MusicStore +{ + public class Startup + { + public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) + { + // Setup configuration sources. + var builder = new ConfigurationBuilder() + .SetBasePath(appEnv.ApplicationBasePath) + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; set; } + + // This method gets called by the runtime. + public void ConfigureServices(IServiceCollection services) + { + services.Configure(settings => + { + settings.DefaultAdminUsername = Configuration["DefaultAdminUsername"]; + settings.DefaultAdminPassword = Configuration["DefaultAdminPassword"]; + }); + + // Add MVC services to the services container. + services.AddMvc(); + + // Uncomment the following line to add Web API services which makes it easier to port Web API 2 controllers. + // You will also need to add the Microsoft.AspNet.Mvc.WebApiCompatShim package to the 'dependencies' section of project.json. + // services.AddWebApiConventions(); + + // Add EF services to the service container + services.AddEntityFramework() + .AddSqlite() + .AddDbContext(options => { + options.UseSqlite(Configuration["DbConnectionString"]); + }); + + // Add Identity services to the services container + services.AddIdentity() + .AddEntityFrameworkStores() + .AddDefaultTokenProviders(); + + // Uncomment the following line to add Web API services which makes it easier to port Web API 2 controllers. + // You will also need to add the Microsoft.AspNet.Mvc.WebApiCompatShim package to the 'dependencies' section of project.json. + // services.AddWebApiConventions(); + + // Configure Auth + services.Configure(options => + { + options.AddPolicy("app-ManageStore", new AuthorizationPolicyBuilder().RequireClaim("app-ManageStore", "Allowed").Build()); + }); + + Mapper.CreateMap(); + Mapper.CreateMap(); + Mapper.CreateMap(); + Mapper.CreateMap(); + Mapper.CreateMap(); + Mapper.CreateMap(); + Mapper.CreateMap(); + Mapper.CreateMap(); + } + + // Configure is called after ConfigureServices is called. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + // Initialize the sample data + SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait(); + + loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.AddConsole(); + loggerFactory.AddDebug(); + + // Configure the HTTP request pipeline. + + // Add the platform handler to the request pipeline. + app.UseIISPlatformHandler(); + + // Add the following to the request pipeline only in development environment. + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + // Add Error handling middleware which catches all application specific errors and + // send the request to the following path or controller action. + app.UseExceptionHandler("/Home/Error"); + } + + var nodeInstance = new NodeInstance(); + app.Use(async (context, next) => { + if (context.Request.Path.Value.EndsWith(".less")) { + // Note: check for directory traversal + var output = await nodeInstance.Invoke("lessCompiler.js", env.WebRootPath + context.Request.Path.Value); + await context.Response.WriteAsync(output); + } else { + await next(); + } + }); + + // Add static files to the request pipeline. + app.UseStaticFiles(); + + // Add MVC to the request pipeline. + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller=Home}/{action=Index}/{id?}"); + + routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" }); + + // Uncomment the following line to add a route for porting Web API 2 controllers. + // routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"); + }); + } + } +} diff --git a/samples/angular/MusicStore/Views/Home/Index.cshtml b/samples/angular/MusicStore/Views/Home/Index.cshtml new file mode 100755 index 000000000000..901925158f5a --- /dev/null +++ b/samples/angular/MusicStore/Views/Home/Index.cshtml @@ -0,0 +1,21 @@ +@{ + ViewData["Title"] = "Home Page"; +} + + + + Loading... + + + +@section scripts { + + + + + + + + + +} diff --git a/samples/angular/MusicStore/Views/Shared/Error.cshtml b/samples/angular/MusicStore/Views/Shared/Error.cshtml new file mode 100755 index 000000000000..a288cb0581f8 --- /dev/null +++ b/samples/angular/MusicStore/Views/Shared/Error.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

diff --git a/samples/angular/MusicStore/Views/Shared/_Layout.cshtml b/samples/angular/MusicStore/Views/Shared/_Layout.cshtml new file mode 100755 index 000000000000..21900d325023 --- /dev/null +++ b/samples/angular/MusicStore/Views/Shared/_Layout.cshtml @@ -0,0 +1,40 @@ + + + + + + Music Store + + + + + + + + + + + + + @RenderBody() + + + + + + + + + + + @RenderSection("scripts", required: false) + + diff --git a/samples/angular/MusicStore/Views/_ViewImports.cshtml b/samples/angular/MusicStore/Views/_ViewImports.cshtml new file mode 100755 index 000000000000..808b5ca460b5 --- /dev/null +++ b/samples/angular/MusicStore/Views/_ViewImports.cshtml @@ -0,0 +1,3 @@ +@using MusicStore +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" +@addTagHelper "*, Microsoft.AspNet.NodeServices.Angular" diff --git a/samples/angular/MusicStore/Views/_ViewStart.cshtml b/samples/angular/MusicStore/Views/_ViewStart.cshtml new file mode 100755 index 000000000000..66b5da255ace --- /dev/null +++ b/samples/angular/MusicStore/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/samples/angular/MusicStore/appsettings.json b/samples/angular/MusicStore/appsettings.json new file mode 100755 index 000000000000..c8d7a432707a --- /dev/null +++ b/samples/angular/MusicStore/appsettings.json @@ -0,0 +1,3 @@ +{ + "DbConnectionString": "Data Source=music-db.sqlite" +} diff --git a/samples/angular/MusicStore/gulpfile.js b/samples/angular/MusicStore/gulpfile.js new file mode 100755 index 000000000000..70895ec7f80d --- /dev/null +++ b/samples/angular/MusicStore/gulpfile.js @@ -0,0 +1,53 @@ +/// + +"use strict"; + +var path = require('path'); +var gulp = require('gulp'); +var del = require('del'); +var eventStream = require('event-stream'); +var typescript = require('gulp-typescript'); +var inlineNg2Template = require('gulp-inline-ng2-template'); +var sourcemaps = require('gulp-sourcemaps'); + +var project = require("./project.json"); +var webroot = "./" + project.webroot + "/"; + +var config = { + libBase: 'node_modules', + lib: [ + require.resolve('bootstrap/dist/css/bootstrap.css'), + path.dirname(require.resolve('bootstrap/dist/fonts/glyphicons-halflings-regular.woff')) + '/**', + require.resolve('traceur/bin/traceur-runtime.js'), + require.resolve('es6-module-loader/dist/es6-module-loader-sans-promises.js'), + require.resolve('reflect-metadata/Reflect.js'), + require.resolve('systemjs/dist/system.src.js'), + require.resolve('angular2/bundles/angular2.dev.js'), + require.resolve('angular2/bundles/router.dev.js'), + require.resolve('angular2/bundles/http.dev.js'), + require.resolve('jquery/dist/jquery.js'), + require.resolve('bootstrap/dist/js/bootstrap.js') + ] +}; + +gulp.task('build.lib', function () { + return gulp.src(config.lib, { base: config.libBase }) + .pipe(gulp.dest(webroot + 'lib')); +}); + +gulp.task('build', ['build.lib'], function () { + var tsProject = typescript.createProject('./tsconfig.json', { typescript: require('typescript') }); + var tsSrcInlined = gulp.src([webroot + '**/*.ts'], { base: webroot }) + .pipe(inlineNg2Template({ base: webroot })); + return eventStream.merge(tsSrcInlined, gulp.src('Typings/**/*.ts')) + .pipe(sourcemaps.init()) + .pipe(typescript(tsProject)) + .pipe(sourcemaps.write()) + .pipe(gulp.dest(webroot)); +}); + +gulp.task('clean', function () { + return del([webroot + 'lib']); +}); + +gulp.task('default', ['build']); diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json new file mode 100644 index 000000000000..f1db973e9169 --- /dev/null +++ b/samples/angular/MusicStore/package.json @@ -0,0 +1,27 @@ +{ + "name": "MusicStore", + "version": "0.0.0", + "dependencies": { + "angular2": "2.0.0-alpha.44", + "angular2-universal-patched": "^0.5.4", + "body-parser": "^1.14.1", + "bootstrap": "^3.3.5", + "del": "^2.0.2", + "es6-module-loader": "^0.15.0", + "express": "^4.13.3", + "jquery": "^2.1.4", + "less": "^2.5.3", + "reflect-metadata": "^0.1.2", + "systemjs": "^0.19.3", + "traceur": "0.0.91" + }, + "devDependencies": { + "del": "^2.0.2", + "event-stream": "^3.3.1", + "gulp": "^3.9.0", + "gulp-inline-ng2-template": "0.0.7", + "gulp-sourcemaps": "^1.6.0", + "gulp-typescript": "^2.9.0", + "typescript": "^1.6.2" + } +} diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json new file mode 100755 index 000000000000..7a9f18c604d6 --- /dev/null +++ b/samples/angular/MusicStore/project.json @@ -0,0 +1,49 @@ +{ + "webroot": "wwwroot", + "version": "1.0.0-*", + "tooling": { + "defaultNamespace": "MusicStore" + }, + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", + "Microsoft.AspNet.Mvc": "6.0.0-beta8", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", + "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", + "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", + "Microsoft.Framework.Logging": "1.0.0-beta8", + "Microsoft.Framework.Logging.Console": "1.0.0-beta8", + "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", + "EntityFramework.SQLite": "7.0.0-beta8", + "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", + "AutoMapper": "4.0.0-alpha1", + "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha1" + }, + "commands": { + "web": "Microsoft.AspNet.Server.Kestrel" + }, + "frameworks": { + "dnx451": {}, + "dnxcore50": {} + }, + "exclude": [ + "wwwroot", + "node_modules", + "bower_components" + ], + "publishExclude": [ + "node_modules", + "bower_components", + "**.xproj", + "**.user", + "**.vspscc" + ], + "scripts": { + "prepublish": [ + "npm install", + "gulp" + ] + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/tsconfig.json b/samples/angular/MusicStore/tsconfig.json new file mode 100644 index 000000000000..a4c5eeb7707c --- /dev/null +++ b/samples/angular/MusicStore/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "sourceMap": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "noLib": false + }, + "exclude": [ + "node_modules" + ] +} diff --git a/samples/angular/MusicStore/wwwroot/css/site.css b/samples/angular/MusicStore/wwwroot/css/site.css new file mode 100644 index 000000000000..952e03fba0f9 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/css/site.css @@ -0,0 +1,3 @@ +body { + padding-top: 50px; +} diff --git a/samples/angular/MusicStore/wwwroot/css/styles.less b/samples/angular/MusicStore/wwwroot/css/styles.less new file mode 100644 index 000000000000..564f8f407a6e --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/css/styles.less @@ -0,0 +1,14 @@ +@base: #f938ab; + +.box-shadow(@style, @c) when (iscolor(@c)) { + -webkit-box-shadow: @style @c; + box-shadow: @style @c; +} +.box-shadow(@style, @alpha: 50%) when (isnumber(@alpha)) { + .box-shadow(@style, rgba(0, 0, 0, @alpha)); +} +.box { + color: saturate(@base, 5%); + border-color: lighten(@base, 30%); + div { .box-shadow(0 0 5px, 30%) } +} diff --git a/samples/angular/MusicStore/wwwroot/images/home-showcase.png b/samples/angular/MusicStore/wwwroot/images/home-showcase.png new file mode 100644 index 0000000000000000000000000000000000000000..258c19d3cd29d8e94fc95877682f45f3190dcb72 GIT binary patch literal 254130 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 literal 0 HcmV?d00001 diff --git a/samples/angular/MusicStore/wwwroot/images/logo.png b/samples/angular/MusicStore/wwwroot/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d334c8625625b78836bb729ed9b7a10fa04bb702 GIT binary patch literal 2963 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 Manager + + diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts new file mode 100644 index 000000000000..73eeafc4d765 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts @@ -0,0 +1,21 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { AlbumsList } from '../albums-list/albums-list'; +import { AlbumDetails } from '../album-details/album-details'; +import { AlbumEdit } from '../album-edit/album-edit'; + +@ng.Component({ + selector: 'admin-home' +}) +@router.RouteConfig([ + { path: 'albums', as: 'Albums', component: AlbumsList }, + { path: 'album/details/:albumId', as: 'AlbumDetails', component: AlbumDetails }, + { path: 'album/edit/:albumId', as: 'AlbumEdit', component: AlbumEdit } +]) +@ng.View({ + templateUrl: './ng-app/components/admin/admin-home/admin-home.html', + directives: [router.ROUTER_DIRECTIVES] +}) +export class AdminHome { + +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html new file mode 100644 index 000000000000..a04250c7eee4 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html @@ -0,0 +1,17 @@ + diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.ts new file mode 100644 index 000000000000..ca2b2e10bdcc --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.ts @@ -0,0 +1,25 @@ +import * as ng from 'angular2/angular2'; +import * as models from '../../../models/models'; + +@ng.Component({ + selector: 'album-delete-prompt' +}) +@ng.View({ + templateUrl: './ng-app/components/admin/album-delete-prompt/album-delete-prompt.html', + directives: [ng.NgIf] +}) +export class AlbumDeletePrompt { + private modalElement: any; + public album: models.Album; + + constructor(@ng.Inject(ng.ElementRef) elementRef: ng.ElementRef) { + if (typeof window !== 'undefined') { + this.modalElement = (window).jQuery(".modal", elementRef.nativeElement); + } + } + + public show(album: models.Album) { + this.album = album; + this.modalElement.modal(); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.html new file mode 100644 index 000000000000..1f66a6371138 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.html @@ -0,0 +1,50 @@ +

Album Details

+
+ +
+
+ +
+

{{ albumData.Artist.Name }}

+
+
+
+ +
+

{{ albumData.Genre.Name }}

+
+
+
+ +
+

{{ albumData.Title }}

+
+
+
+ +
+

{{ albumData.Price | currency:'USD':true }}

+
+
+
+ +
+

{{ albumData.AlbumArtUrl }}

+
+
+
+ +
+ {{ albumData.Title }} +
+
+
+
+ Edit + + Back to List +
+
+
+ + \ No newline at end of file diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.ts new file mode 100644 index 000000000000..bf38fe4d240c --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-details/album-details.ts @@ -0,0 +1,22 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import * as models from '../../../models/models'; +import { Http, HTTP_BINDINGS } from 'angular2/http'; +import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; + +@ng.Component({ + selector: 'album-details' +}) +@ng.View({ + templateUrl: './ng-app/components/admin/album-details/album-details.html', + directives: [router.ROUTER_DIRECTIVES, ng.NgIf, AlbumDeletePrompt] +}) +export class AlbumDetails { + public albumData: models.Album; + + constructor(http: Http, routeParam: router.RouteParams) { + http.get('/api/albums/' + routeParam.params['albumId']).subscribe(result => { + this.albumData = result.json(); + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html new file mode 100644 index 000000000000..1276a38f0dde --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html @@ -0,0 +1,45 @@ +

Album Edit

+
+ +
+ + + + + + + + + + + + + +
+ $ + +
+
+ + + + + + + + + + + + + Back to List + +
+ + \ No newline at end of file diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts new file mode 100644 index 000000000000..e199e22461a9 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -0,0 +1,82 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import * as models from '../../../models/models'; +import { Http, HTTP_BINDINGS, Headers } from 'angular2/http'; +import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; +import { FormField } from '../form-field/form-field'; + +@ng.Component({ + selector: 'album-edit' +}) +@ng.View({ + templateUrl: './ng-app/components/admin/album-edit/album-edit.html', + directives: [router.ROUTER_DIRECTIVES, ng.NgIf, ng.NgFor, AlbumDeletePrompt, FormField, ng.FORM_DIRECTIVES] +}) +export class AlbumEdit { + public form: ng.ControlGroup; + public artists: models.Artist[]; + public genres: models.Genre[]; + public originalAlbum: models.Album; + + private _http: Http; + + constructor(fb: ng.FormBuilder, http: Http, routeParam: router.RouteParams) { + this._http = http; + + http.get('/api/albums/' + routeParam.params['albumId']).subscribe(result => { + var json = result.json(); + this.originalAlbum = json; + (this.form.controls['title']).updateValue(json.Title); + (this.form.controls['price']).updateValue(json.Price); + (this.form.controls['artist']).updateValue(json.ArtistId); + (this.form.controls['genre']).updateValue(json.GenreId); + (this.form.controls['albumArtUrl']).updateValue(json.AlbumArtUrl); + }); + + http.get('/api/artists/lookup').subscribe(result => { + this.artists = result.json(); + }); + + http.get('/api/genres/genre-lookup').subscribe(result => { + this.genres = result.json(); + }); + + this.form = fb.group({ + artist: fb.control('', ng.Validators.required), + genre: fb.control('', ng.Validators.required), + title: fb.control('', ng.Validators.required), + price: fb.control('', ng.Validators.compose([ng.Validators.required, AlbumEdit._validatePrice])), + albumArtUrl: fb.control('', ng.Validators.required) + }); + } + + public onSubmitModelBased() { + // Force all fields to show any validation errors even if they haven't been touched + Object.keys(this.form.controls).forEach(controlName => { + this.form.controls[controlName].markAsTouched(); + }); + + if (this.form.valid) { + var controls = this.form.controls; + var albumId = this.originalAlbum.AlbumId; + (window).fetch(`/api/albums/${ albumId }/update`, { + method: 'put', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + AlbumArtUrl: controls['albumArtUrl'].value, + AlbumId: albumId, + ArtistId: controls['artist'].value, + GenreId: controls['genre'].value, + Price: controls['price'].value, + Title: controls['title'].value + }) + }).then(response => { + console.log(response); + }); + } + } + + private static _validatePrice(control: ng.Control): { [key: string]: boolean } { + return /^\d+\.\d+$/.test(control.value) ? null : { price: true }; + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.html new file mode 100644 index 000000000000..ac3a4e61beb2 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.html @@ -0,0 +1,44 @@ +

Albums

+ + + + + + + + + + + + + + + + + + + + + + +
GenreArtistTitlePrice
{{ row.Genre.Name }}{{ row.Artist.Name }}{{ row.Title }}{{ row.Price | currency:'USD':true }} +
+ Details + Edit + Delete +
+
+ +
+ + + + + +
+ +

{{ totalCount }} total albums

diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.ts new file mode 100644 index 000000000000..49b2f5515e25 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/albums-list/albums-list.ts @@ -0,0 +1,70 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { Http, HTTP_BINDINGS } from 'angular2/http'; +import * as models from '../../../models/models'; +import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; + +@ng.Component({ + selector: 'albums-list' +}) +@ng.View({ + templateUrl: './ng-app/components/admin/albums-list/albums-list.html', + directives: [ng.NgFor, ng.NgClass, router.ROUTER_DIRECTIVES, AlbumDeletePrompt] +}) +export class AlbumsList { + public rows: models.Album[]; + public canGoBack: boolean; + public canGoForward: boolean; + public pageLinks: any[]; + public totalCount: number; + public get pageIndex() { + return this._pageIndex; + } + + private _http: Http; + private _pageIndex = 1; + private _sortBy = "Title"; + private _sortByDesc = false; + + constructor(http: Http) { + this._http = http; + this.refreshData(); + } + + public sortBy(col: string) { + this._sortByDesc = col === this._sortBy ? !this._sortByDesc : false; + this._sortBy = col; + this.refreshData(); + } + + public goToPage(pageIndex: number) { + this._pageIndex = pageIndex; + this.refreshData(); + } + + public goToLast() { + this.goToPage(this.pageLinks[this.pageLinks.length - 1].index); + } + + refreshData() { + var sortBy = this._sortBy + (this._sortByDesc ? ' DESC' : ''); + this._http.get(`/api/albums?page=${ this._pageIndex }&pageSize=50&sortBy=${ sortBy }`).subscribe(result => { + var json = result.json(); + this.rows = json.Data; + + var numPages = Math.ceil(json.TotalCount / json.PageSize); + this.pageLinks = []; + for (var i = 1; i <= numPages; i++) { + this.pageLinks.push({ + index: i, + text: i.toString(), + isCurrent: i === json.Page + }); + } + + this.canGoBack = this.pageLinks.length && !this.pageLinks[0].isCurrent; + this.canGoForward = this.pageLinks.length && !this.pageLinks[this.pageLinks.length - 1].isCurrent; + this.totalCount = json.TotalCount; + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.html new file mode 100644 index 000000000000..b2ba22d13c9c --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.html @@ -0,0 +1,9 @@ +
+ +
+ + +
+
diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts new file mode 100644 index 000000000000..7cf7a3007cb7 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts @@ -0,0 +1,20 @@ +import * as ng from 'angular2/angular2'; + +@ng.Component({ + selector: 'form-field', + properties: ['label', 'validate'] +}) +@ng.View({ + templateUrl: './ng-app/components/admin/form-field/form-field.html', + directives: [ng.NgIf, ng.NgFor] +}) +export class FormField { + private validate: ng.AbstractControl; + + public get errorMessages() { + var errors = (this.validate && this.validate.touched && this.validate.errors) || {}; + return Object.keys(errors).map(key => { + return 'Error: ' + key; + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.css b/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.css new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.html b/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.html new file mode 100644 index 000000000000..6d620ad29cf2 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.html @@ -0,0 +1,30 @@ + +
+ +
diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.ts new file mode 100644 index 000000000000..1da7523dc62d --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/app/app.ts @@ -0,0 +1,35 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { Http, HTTP_BINDINGS } from 'angular2/http'; +import { Home } from '../public/home/home'; +import { AlbumDetails } from '../public/album-details/album-details'; +import { GenreContents } from '../public/genre-contents/genre-contents'; +import { GenresList } from '../public/genres-list/genres-list'; +import { AdminHome } from '../admin/admin-home/admin-home'; +import * as models from '../../models/models'; + +@ng.Component({ + selector: 'app' +}) +@router.RouteConfig([ + { path: '/', component: Home, as: 'Home' }, + { path: '/album/:albumId', component: AlbumDetails, as: 'Album' }, + { path: '/genre/:genreId', component: GenreContents, as: 'Genre' }, + { path: '/genres', component: GenresList, as: 'GenresList' }, + { path: '/admin/...', component: AdminHome, as: 'Admin' } +]) +@ng.View({ + templateUrl: './ng-app/components/app/app.html', + styleUrls: ['./ng-app/components/app/app.css'], + directives: [router.ROUTER_DIRECTIVES, ng.NgFor] +}) +export class App { + public genres: models.Genre[]; + + constructor(http: Http) { + http.get('/api/genres/menu').subscribe(result => { + this.genres = result.json(); + }); + } +} + diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts new file mode 100644 index 000000000000..a7d6cf3955a8 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts @@ -0,0 +1,6 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { Http, HTTP_BINDINGS } from 'angular2/http'; +import { App } from './app'; + +ng.bootstrap(App, [router.ROUTER_BINDINGS, HTTP_BINDINGS, ng.FormBuilder]); diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.html b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.html new file mode 100644 index 000000000000..b05a0f79d773 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.html @@ -0,0 +1,26 @@ +
+

{{ albumData.Title }}

+ +

+ {{ albumData.Title }} +

+ +
+

+ Genre: + {{ albumData.Genre.Name }} +

+

+ Artist: + {{ albumData.Artist.Name }} +

+

+ Price: + {{ albumData.Price | currency:'USD':true }} +

+

+ + Add to cart +

+
+
\ No newline at end of file diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.ts new file mode 100644 index 000000000000..96c869bdadf5 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-details/album-details.ts @@ -0,0 +1,21 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { Http } from 'angular2/http'; +import * as models from '../../../models/models'; + +@ng.Component({ + selector: 'album-details' +}) +@ng.View({ + templateUrl: './ng-app/components/public/album-details/album-details.html', + directives: [ng.NgIf] +}) +export class AlbumDetails { + public albumData: models.Album; + + constructor(http: Http, routeParam: router.RouteParams) { + http.get('/api/albums/' + routeParam.params['albumId']).subscribe(result => { + this.albumData = result.json(); + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.html b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.html new file mode 100644 index 000000000000..00edd7593ae7 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.html @@ -0,0 +1,4 @@ + + {{ albumData.Title }} +

{{ albumData.Title }}

+
diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.ts new file mode 100644 index 000000000000..345a60b78efc --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/album-tile/album-tile.ts @@ -0,0 +1,14 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import * as models from '../../../models/models'; + +@ng.Component({ + selector: 'album-tile', + properties: ['albumData: albumdata'] +}) +@ng.View({ + templateUrl: './ng-app/components/public/album-tile/album-tile.html', + directives: [router.ROUTER_DIRECTIVES] +}) +export class AlbumTile { +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.html b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.html new file mode 100644 index 000000000000..1911ae53cb7f --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.html @@ -0,0 +1,7 @@ +

Albums

+ +
    +
  • + +
  • +
diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.ts new file mode 100644 index 000000000000..814ac4996150 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genre-contents/genre-contents.ts @@ -0,0 +1,22 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { Http } from 'angular2/http'; +import * as models from '../../../models/models'; +import { AlbumTile } from '../album-tile/album-tile'; + +@ng.Component({ + selector: 'genre-contents' +}) +@ng.View({ + templateUrl: './ng-app/components/public/genre-contents/genre-contents.html', + directives: [ng.NgFor, AlbumTile] +}) +export class GenreContents { + public albums: models.Album[]; + + constructor(http: Http, routeParam: router.RouteParams) { + http.get(`/api/genres/${ routeParam.params['genreId'] }/albums`).subscribe(result => { + this.albums = result.json(); + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.html b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.html new file mode 100644 index 000000000000..dc87efd9cd50 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.html @@ -0,0 +1,13 @@ +

Browse Genres

+ +

+ Select from {{ genres.length }} genres: +

+ + diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.ts new file mode 100644 index 000000000000..f7742b0f2dc6 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/genres-list/genres-list.ts @@ -0,0 +1,21 @@ +import * as ng from 'angular2/angular2'; +import * as router from 'angular2/router'; +import { Http } from 'angular2/http'; +import * as models from '../../../models/models'; + +@ng.Component({ + selector: 'genres-list' +}) +@ng.View({ + templateUrl: './ng-app/components/public/genres-list/genres-list.html', + directives: [router.ROUTER_DIRECTIVES, ng.NgIf, ng.NgFor] +}) +export class GenresList { + public genres: models.Genre[]; + + constructor(http: Http) { + http.get('/api/genres').subscribe(result => { + this.genres = result.json(); + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.html b/samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.html new file mode 100644 index 000000000000..b0fbb3d08686 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.html @@ -0,0 +1,10 @@ +
+

MVC Music Store

+ +
+ +
    +
  • + +
  • +
diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.ts new file mode 100644 index 000000000000..cf1ad9fff626 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/public/home/home.ts @@ -0,0 +1,21 @@ +import * as ng from 'angular2/angular2'; +import { Http } from 'angular2/http'; +import { AlbumTile } from '../album-tile/album-tile'; +import * as models from '../../../models/models'; + +@ng.Component({ + selector: 'home' +}) +@ng.View({ + templateUrl: './ng-app/components/public/home/home.html', + directives: [ng.NgFor, AlbumTile] +}) +export class Home { + public mostPopular: models.Album[]; + + constructor(http: Http) { + http.get('/api/albums/mostPopular').subscribe(result => { + this.mostPopular = result.json(); + }); + } +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/models/models.ts b/samples/angular/MusicStore/wwwroot/ng-app/models/models.ts new file mode 100644 index 000000000000..44c705af7e3e --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/models/models.ts @@ -0,0 +1,16 @@ +export interface Album { + AlbumId: number; + Title: string; + AlbumArtUrl: string; +} + +export interface Genre { + GenreId: number; + Name: string; + Description: string; +} + +export interface Artist { + ArtistId: number; + Name: string; +} diff --git a/samples/angular/MusicStore/wwwroot/system.config.js b/samples/angular/MusicStore/wwwroot/system.config.js new file mode 100644 index 000000000000..400211f4ee58 --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/system.config.js @@ -0,0 +1,3 @@ +System.config({ + defaultJSExtensions: true +}); diff --git a/samples/angular/MusicStore/wwwroot/web.config b/samples/angular/MusicStore/wwwroot/web.config new file mode 100644 index 000000000000..db6e6f4582dc --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/web.config @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/samples/react/ReactGrid/.gitignore b/samples/react/ReactGrid/.gitignore new file mode 100644 index 000000000000..ef7d547f15e5 --- /dev/null +++ b/samples/react/ReactGrid/.gitignore @@ -0,0 +1,5 @@ +/node_modules/ +project.lock.json +/wwwroot/bundle.* +/wwwroot/*.svg +/wwwroot/*.css diff --git a/samples/react/ReactGrid/Controllers/HomeController.cs b/samples/react/ReactGrid/Controllers/HomeController.cs new file mode 100755 index 000000000000..dd9a1abfec28 --- /dev/null +++ b/samples/react/ReactGrid/Controllers/HomeController.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.NodeServices.React; + +namespace ReactExample.Controllers +{ + public class HomeController : Controller + { + public async Task Index(int pageIndex) + { + ViewData["ReactOutput"] = await ReactRenderer.RenderToString( + moduleName: "ReactApp/components/ReactApp.jsx", + exportName: "ReactApp", + baseUrl: Request.Path + ); + return View(); + } + + public IActionResult Error() + { + return View("~/Views/Shared/Error.cshtml"); + } + } +} diff --git a/samples/react/ReactGrid/README.txt b/samples/react/ReactGrid/README.txt new file mode 100644 index 000000000000..e702b30833bc --- /dev/null +++ b/samples/react/ReactGrid/README.txt @@ -0,0 +1,2 @@ +Portions of this sample application (particularly, the fake data) are based +on https://github.com/DavidWells/isomorphic-react-example diff --git a/samples/react/ReactGrid/ReactApp/boot-client.jsx b/samples/react/ReactGrid/ReactApp/boot-client.jsx new file mode 100644 index 000000000000..623daf48fb8d --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/boot-client.jsx @@ -0,0 +1,8 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import createBrowserHistory from 'history/lib/createBrowserHistory'; +import { ReactApp } from './components/ReactApp.jsx'; + +// In the browser, we render into a DOM node and hook up to the browser's history APIs +var history = createBrowserHistory(); +ReactDOM.render(, document.getElementById('react-app')); diff --git a/samples/react/ReactGrid/ReactApp/components/CustomPager.jsx b/samples/react/ReactGrid/ReactApp/components/CustomPager.jsx new file mode 100644 index 000000000000..44ce1b566008 --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/components/CustomPager.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Link } from 'react-router'; + +export class CustomPager extends React.Component { + pageChange(event) { + this.props.setPage(parseInt(event.target.getAttribute("data-value"))); + } + + render() { + var previous = ""; + var next = ""; + + if(this.props.currentPage > 0){ + previous =
{this.props.previousText}
; + } + + if(this.props.currentPage != (this.props.maxPage -1)){ + next =
{this.props.nextText}
; + } + + var options = []; + + var startIndex = Math.max(this.props.currentPage - 5, 0); + var endIndex = Math.min(startIndex + 11, this.props.maxPage); + + if (this.props.maxPage >= 11 && (endIndex - startIndex) <= 10) { + startIndex = endIndex - 11; + } + + for(var i = startIndex; i < endIndex ; i++){ + var selected = this.props.currentPage == i ? "btn-default" : ""; + options.push(
{i+1}
); + } + + return ( +
+ {previous} + {options} + {next} +
+ ); + } +} + +CustomPager.defaultProps = { + maxPage: 0, + nextText: '', + previousText: '', + currentPage: 0 +}; \ No newline at end of file diff --git a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx new file mode 100644 index 000000000000..c6662fae729b --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Griddle from 'griddle-react'; +import { CustomPager } from './CustomPager.jsx'; +import { fakeData } from '../data/fakeData.js'; +import { columnMeta } from '../data/columnMeta.js'; +const resultsPerPage = 10; + +export class PeopleGrid extends React.Component { + render() { + var pageIndex = this.props.params ? (this.props.params.pageIndex || 1) - 1 : 0; + return ( +
+

People

+
+ +
+
+ ); + } +} diff --git a/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx b/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx new file mode 100644 index 000000000000..45c9bf11aa93 --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { Router, Route } from 'react-router'; +import { PeopleGrid } from './PeopleGrid.jsx'; + +export class ReactApp extends React.Component { + render() { + return ( + + + + + ); + } +} diff --git a/samples/react/ReactGrid/ReactApp/data/columnMeta.js b/samples/react/ReactGrid/ReactApp/data/columnMeta.js new file mode 100644 index 000000000000..f470451a68dd --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/data/columnMeta.js @@ -0,0 +1,47 @@ +var columnMeta = [ + { + "columnName": "id", + "order": 1, + "locked": false, + "visible": true + }, + { + "columnName": "name", + "order": 2, + "locked": false, + "visible": true + }, + { + "columnName": "city", + "order": 3, + "locked": false, + "visible": true + }, + { + "columnName": "state", + "order": 4, + "locked": false, + "visible": true + }, + { + "columnName": "country", + "order": 5, + "locked": false, + "visible": true + }, + { + "columnName": "company", + "order": 6, + "locked": false, + "visible": true + }, + { + "columnName": "favoriteNumber", + "order": 7, + "locked": false, + "visible": true + } +]; + +export var columnMeta; + diff --git a/samples/react/ReactGrid/ReactApp/data/fakeData.js b/samples/react/ReactGrid/ReactApp/data/fakeData.js new file mode 100644 index 000000000000..d959fb7abd8a --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/data/fakeData.js @@ -0,0 +1,2489 @@ +var fakeData = [ + { + "id": 0, + "name": "Mayer Leonard", + "city": "Kapowsin", + "state": "Hawaii", + "country": "United Kingdom", + "company": "Ovolo", + "favoriteNumber": 7 + }, + { + "id": 1, + "name": "Koch Becker", + "city": "Johnsonburg", + "state": "New Jersey", + "country": "Madagascar", + "company": "Eventage", + "favoriteNumber": 2 + }, + { + "id": 2, + "name": "Lowery Hopkins", + "city": "Blanco", + "state": "Arizona", + "country": "Ukraine", + "company": "Comtext", + "favoriteNumber": 3 + }, + { + "id": 3, + "name": "Walters Mays", + "city": "Glendale", + "state": "Illinois", + "country": "New Zealand", + "company": "Corporana", + "favoriteNumber": 6 + }, + { + "id": 4, + "name": "Shaw Lowe", + "city": "Coultervillle", + "state": "Wyoming", + "country": "Ecuador", + "company": "Isologica", + "favoriteNumber": 2 + }, + { + "id": 5, + "name": "Ola Fernandez", + "city": "Deltaville", + "state": "Delaware", + "country": "Virgin Islands (US)", + "company": "Pawnagra", + "favoriteNumber": 7 + }, + { + "id": 6, + "name": "Park Carr", + "city": "Welda", + "state": "Kentucky", + "country": "Sri Lanka", + "company": "Cosmetex", + "favoriteNumber": 7 + }, + { + "id": 7, + "name": "Laverne Johnson", + "city": "Rosburg", + "state": "New Mexico", + "country": "Croatia", + "company": "Housedown", + "favoriteNumber": 9 + }, + { + "id": 8, + "name": "Lizzie Nelson", + "city": "Chumuckla", + "state": "Montana", + "country": "Turks & Caicos", + "company": "Everest", + "favoriteNumber": 2 + }, + { + "id": 9, + "name": "Clarke Clemons", + "city": "Inkerman", + "state": "Rhode Island", + "country": "Cambodia", + "company": "Apexia", + "favoriteNumber": 3 + }, + { + "id": 10, + "name": "Cindy Phelps", + "city": "Hachita", + "state": "North Carolina", + "country": "Namibia", + "company": "Pholio", + "favoriteNumber": 6 + }, + { + "id": 11, + "name": "Danielle Keller", + "city": "Stockdale", + "state": "Maryland", + "country": "Cape Verde", + "company": "Netility", + "favoriteNumber": 10 + }, + { + "id": 12, + "name": "Duke Hutchinson", + "city": "Needmore", + "state": "Indiana", + "country": "Brunei", + "company": "Electonic", + "favoriteNumber": 1 + }, + { + "id": 13, + "name": "Aimee Duffy", + "city": "Brownlee", + "state": "Vermont", + "country": "Lebanon", + "company": "Repetwire", + "favoriteNumber": 2 + }, + { + "id": 14, + "name": "Meadows Jimenez", + "city": "Winesburg", + "state": "Kansas", + "country": "Timor L'Este", + "company": "Quonk", + "favoriteNumber": 0 + }, + { + "id": 15, + "name": "Karla Potts", + "city": "Juarez", + "state": "Alaska", + "country": "Samoa", + "company": "Zentime", + "favoriteNumber": 3 + }, + { + "id": 16, + "name": "Rita Jensen", + "city": "Elwood", + "state": "North Dakota", + "country": "Greece", + "company": "Valpreal", + "favoriteNumber": 9 + }, + { + "id": 17, + "name": "Jackie Burke", + "city": "Delwood", + "state": "Arkansas", + "country": "Greenland", + "company": "Magmina", + "favoriteNumber": 4 + }, + { + "id": 18, + "name": "Corinne Moreno", + "city": "Wollochet", + "state": "New Hampshire", + "country": "Sierra Leone", + "company": "Marketoid", + "favoriteNumber": 1 + }, + { + "id": 19, + "name": "Giles Cohen", + "city": "Carbonville", + "state": "Massachusetts", + "country": "Tonga", + "company": "Ginkogene", + "favoriteNumber": 10 + }, + { + "id": 20, + "name": "Maynard Barnes", + "city": "Boling", + "state": "Utah", + "country": "Nepal", + "company": "Kyaguru", + "favoriteNumber": 8 + }, + { + "id": 21, + "name": "Singleton Lindsay", + "city": "Weogufka", + "state": "Tennessee", + "country": "Falkland Islands", + "company": "Egypto", + "favoriteNumber": 5 + }, + { + "id": 22, + "name": "Etta Kemp", + "city": "Como", + "state": "Pennsylvania", + "country": "Syria", + "company": "Marqet", + "favoriteNumber": 3 + }, + { + "id": 23, + "name": "Whitney Pennington", + "city": "Farmington", + "state": "Louisiana", + "country": "Suriname", + "company": "Prosure", + "favoriteNumber": 10 + }, + { + "id": 24, + "name": "Sophie Ellison", + "city": "Whitewater", + "state": "Idaho", + "country": "Malta", + "company": "Evidends", + "favoriteNumber": 1 + }, + { + "id": 25, + "name": "Logan Forbes", + "city": "Idledale", + "state": "Michigan", + "country": "Dominican Republic", + "company": "Pigzart", + "favoriteNumber": 3 + }, + { + "id": 26, + "name": "Haley Mcclure", + "city": "Eggertsville", + "state": "Colorado", + "country": "Honduras", + "company": "Ginkle", + "favoriteNumber": 8 + }, + { + "id": 27, + "name": "Williamson Hurley", + "city": "Edgar", + "state": "Texas", + "country": "Yemen", + "company": "Tetratrex", + "favoriteNumber": 3 + }, + { + "id": 28, + "name": "Heidi Hurst", + "city": "Curtice", + "state": "Nebraska", + "country": "Aruba", + "company": "Vendblend", + "favoriteNumber": 10 + }, + { + "id": 29, + "name": "Barker Long", + "city": "Orovada", + "state": "West Virginia", + "country": "Egypt", + "company": "Uniworld", + "favoriteNumber": 8 + }, + { + "id": 30, + "name": "Richard Patrick", + "city": "Gordon", + "state": "Oregon", + "country": "Malawi", + "company": "Quarx", + "favoriteNumber": 8 + }, + { + "id": 31, + "name": "Cameron Graham", + "city": "Noblestown", + "state": "Oklahoma", + "country": "Slovenia", + "company": "Zilidium", + "favoriteNumber": 5 + }, + { + "id": 32, + "name": "Lucy Quinn", + "city": "Greenock", + "state": "Ohio", + "country": "Australia", + "company": "Geoform", + "favoriteNumber": 10 + }, + { + "id": 33, + "name": "Dickson Greene", + "city": "Jeff", + "state": "Virginia", + "country": "Iraq", + "company": "Niquent", + "favoriteNumber": 6 + }, + { + "id": 34, + "name": "Jasmine Brock", + "city": "Tolu", + "state": "Mississippi", + "country": "Hungary", + "company": "Cytrek", + "favoriteNumber": 8 + }, + { + "id": 35, + "name": "Byers Donaldson", + "city": "Jugtown", + "state": "South Dakota", + "country": "Mongolia", + "company": "Slambda", + "favoriteNumber": 4 + }, + { + "id": 36, + "name": "Burns Blake", + "city": "Shawmut", + "state": "Iowa", + "country": "Ethiopia", + "company": "Comstar", + "favoriteNumber": 9 + }, + { + "id": 37, + "name": "Norman Wynn", + "city": "Hasty", + "state": "Washington", + "country": "Bangladesh", + "company": "Netplode", + "favoriteNumber": 7 + }, + { + "id": 38, + "name": "Anthony Weeks", + "city": "Chautauqua", + "state": "Florida", + "country": "Sudan", + "company": "Rubadub", + "favoriteNumber": 9 + }, + { + "id": 39, + "name": "Courtney Marshall", + "city": "Grazierville", + "state": "California", + "country": "Zambia", + "company": "Medicroix", + "favoriteNumber": 0 + }, + { + "id": 40, + "name": "Wilda Foster", + "city": "Ebro", + "state": "New York", + "country": "Cameroon", + "company": "Xixan", + "favoriteNumber": 0 + }, + { + "id": 41, + "name": "Buckner Hyde", + "city": "Century", + "state": "Minnesota", + "country": "Mexico", + "company": "Plasmos", + "favoriteNumber": 6 + }, + { + "id": 42, + "name": "Montgomery Woodard", + "city": "Nadine", + "state": "Georgia", + "country": "Zimbabwe", + "company": "Neptide", + "favoriteNumber": 1 + }, + { + "id": 43, + "name": "Shirley Boyle", + "city": "Groveville", + "state": "Connecticut", + "country": "Tunisia", + "company": "Interodeo", + "favoriteNumber": 1 + }, + { + "id": 44, + "name": "Mavis Welch", + "city": "Springhill", + "state": "South Carolina", + "country": "Italy", + "company": "Asimiline", + "favoriteNumber": 9 + }, + { + "id": 45, + "name": "Barr Flowers", + "city": "Bowden", + "state": "Missouri", + "country": "South Korea", + "company": "Terragen", + "favoriteNumber": 7 + }, + { + "id": 46, + "name": "Cabrera Koch", + "city": "Wanship", + "state": "Maine", + "country": "Mauritius", + "company": "Norsul", + "favoriteNumber": 9 + }, + { + "id": 47, + "name": "Williams Gamble", + "city": "Homestead", + "state": "Wisconsin", + "country": "Romania", + "company": "Gynk", + "favoriteNumber": 4 + }, + { + "id": 48, + "name": "Angelica Washington", + "city": "Roulette", + "state": "Alabama", + "country": "South Africa", + "company": "Exoswitch", + "favoriteNumber": 3 + }, + { + "id": 49, + "name": "Morse Navarro", + "city": "Balm", + "state": "Hawaii", + "country": "Malaysia", + "company": "Comtours", + "favoriteNumber": 7 + }, + { + "id": 50, + "name": "Harding Chambers", + "city": "Lupton", + "state": "New Jersey", + "country": "Oman", + "company": "Gadtron", + "favoriteNumber": 6 + }, + { + "id": 51, + "name": "Frederick Mcdowell", + "city": "Kimmell", + "state": "Arizona", + "country": "Ireland", + "company": "Delphide", + "favoriteNumber": 2 + }, + { + "id": 52, + "name": "Valentine Turner", + "city": "Hobucken", + "state": "Illinois", + "country": "France", + "company": "Sloganaut", + "favoriteNumber": 0 + }, + { + "id": 53, + "name": "Ruby Cooper", + "city": "Connerton", + "state": "Wyoming", + "country": "Iceland", + "company": "Exospace", + "favoriteNumber": 5 + }, + { + "id": 54, + "name": "Natalia Nielsen", + "city": "Holtville", + "state": "Delaware", + "country": "Equatorial Guinea", + "company": "Isoswitch", + "favoriteNumber": 6 + }, + { + "id": 55, + "name": "Bobbie Silva", + "city": "Fivepointville", + "state": "Kentucky", + "country": "Luxembourg", + "company": "Futuris", + "favoriteNumber": 0 + }, + { + "id": 56, + "name": "Clarice Hays", + "city": "Floriston", + "state": "New Mexico", + "country": "Cruise Ship", + "company": "Skyplex", + "favoriteNumber": 5 + }, + { + "id": 57, + "name": "Leblanc Bartlett", + "city": "Catherine", + "state": "Montana", + "country": "Belarus", + "company": "Ezentia", + "favoriteNumber": 10 + }, + { + "id": 58, + "name": "Jodie Martinez", + "city": "Edneyville", + "state": "Rhode Island", + "country": "Antigua & Barbuda", + "company": "Satiance", + "favoriteNumber": 7 + }, + { + "id": 59, + "name": "Pennington Townsend", + "city": "Ahwahnee", + "state": "North Carolina", + "country": "Chad", + "company": "Orbiflex", + "favoriteNumber": 8 + }, + { + "id": 60, + "name": "Garrison Buchanan", + "city": "Coinjock", + "state": "Maryland", + "country": "Reunion", + "company": "Zanity", + "favoriteNumber": 3 + }, + { + "id": 61, + "name": "Cardenas Reeves", + "city": "Greensburg", + "state": "Indiana", + "country": "Gabon", + "company": "Cogentry", + "favoriteNumber": 1 + }, + { + "id": 62, + "name": "Angeline Jacobson", + "city": "Freeburn", + "state": "Vermont", + "country": "Fiji", + "company": "Pearlessa", + "favoriteNumber": 4 + }, + { + "id": 63, + "name": "Turner Franks", + "city": "Fairforest", + "state": "Kansas", + "country": "New Caledonia", + "company": "Maximind", + "favoriteNumber": 1 + }, + { + "id": 64, + "name": "Murphy Santos", + "city": "Waiohinu", + "state": "Alaska", + "country": "Haiti", + "company": "Isodrive", + "favoriteNumber": 0 + }, + { + "id": 65, + "name": "Walls Cherry", + "city": "Avalon", + "state": "North Dakota", + "country": "Mozambique", + "company": "Bolax", + "favoriteNumber": 10 + }, + { + "id": 66, + "name": "Carney Olson", + "city": "Nanafalia", + "state": "Arkansas", + "country": "Pakistan", + "company": "Unq", + "favoriteNumber": 10 + }, + { + "id": 67, + "name": "Jennings Bowers", + "city": "Kenwood", + "state": "New Hampshire", + "country": "Cayman Islands", + "company": "Deepends", + "favoriteNumber": 10 + }, + { + "id": 68, + "name": "Browning Wooten", + "city": "Jessie", + "state": "Massachusetts", + "country": "Guam", + "company": "Eventex", + "favoriteNumber": 5 + }, + { + "id": 69, + "name": "Preston Britt", + "city": "Dennard", + "state": "Utah", + "country": "Cyprus", + "company": "Sureplex", + "favoriteNumber": 4 + }, + { + "id": 70, + "name": "Holly Martin", + "city": "Carrizo", + "state": "Tennessee", + "country": "Nicaragua", + "company": "Sonique", + "favoriteNumber": 1 + }, + { + "id": 71, + "name": "Zelma Barker", + "city": "Zarephath", + "state": "Pennsylvania", + "country": "Czech Republic", + "company": "Xanide", + "favoriteNumber": 9 + }, + { + "id": 72, + "name": "Burgess Zamora", + "city": "Tampico", + "state": "Louisiana", + "country": "Poland", + "company": "Isopop", + "favoriteNumber": 10 + }, + { + "id": 73, + "name": "Galloway Rich", + "city": "Zeba", + "state": "Idaho", + "country": "Uzbekistan", + "company": "Dragbot", + "favoriteNumber": 4 + }, + { + "id": 74, + "name": "Morris Lott", + "city": "Wattsville", + "state": "Michigan", + "country": "Turkmenistan", + "company": "Slumberia", + "favoriteNumber": 3 + }, + { + "id": 75, + "name": "Paul Mcleod", + "city": "Glenbrook", + "state": "Colorado", + "country": "Cuba", + "company": "Candecor", + "favoriteNumber": 6 + }, + { + "id": 76, + "name": "Phoebe Orr", + "city": "Holcombe", + "state": "Texas", + "country": "Faroe Islands", + "company": "Cubicide", + "favoriteNumber": 4 + }, + { + "id": 77, + "name": "Dalton Christensen", + "city": "Rossmore", + "state": "Nebraska", + "country": "Belgium", + "company": "Enormo", + "favoriteNumber": 4 + }, + { + "id": 78, + "name": "Flora Goff", + "city": "Gila", + "state": "West Virginia", + "country": "Philippines", + "company": "Miracula", + "favoriteNumber": 4 + }, + { + "id": 79, + "name": "Sheree Ross", + "city": "Welch", + "state": "Oregon", + "country": "French Polynesia", + "company": "Illumity", + "favoriteNumber": 0 + }, + { + "id": 80, + "name": "Nita Jefferson", + "city": "Calverton", + "state": "Oklahoma", + "country": "Estonia", + "company": "Cincyr", + "favoriteNumber": 2 + }, + { + "id": 81, + "name": "Elma Mendoza", + "city": "Cornfields", + "state": "Ohio", + "country": "Botswana", + "company": "Isotronic", + "favoriteNumber": 6 + }, + { + "id": 82, + "name": "Garcia Hensley", + "city": "Kohatk", + "state": "Virginia", + "country": "Congo", + "company": "Plasmox", + "favoriteNumber": 4 + }, + { + "id": 83, + "name": "Delgado Osborn", + "city": "Nescatunga", + "state": "Mississippi", + "country": "Montenegro", + "company": "Magneato", + "favoriteNumber": 1 + }, + { + "id": 84, + "name": "Chavez Simmons", + "city": "Roderfield", + "state": "South Dakota", + "country": "Norway", + "company": "Waab", + "favoriteNumber": 1 + }, + { + "id": 85, + "name": "Stuart Roach", + "city": "Hebron", + "state": "Iowa", + "country": "Georgia", + "company": "Applica", + "favoriteNumber": 0 + }, + { + "id": 86, + "name": "Georgia Henson", + "city": "Greenbackville", + "state": "Washington", + "country": "Guinea Bissau", + "company": "Talkalot", + "favoriteNumber": 7 + }, + { + "id": 87, + "name": "Ila Sanders", + "city": "Zortman", + "state": "Florida", + "country": "Brazil", + "company": "Koffee", + "favoriteNumber": 10 + }, + { + "id": 88, + "name": "Shepard Maldonado", + "city": "Lawrence", + "state": "California", + "country": "Netherlands", + "company": "Knowlysis", + "favoriteNumber": 1 + }, + { + "id": 89, + "name": "Ramirez Collins", + "city": "Healy", + "state": "New York", + "country": "Guernsey", + "company": "Entroflex", + "favoriteNumber": 4 + }, + { + "id": 90, + "name": "Magdalena Mcgee", + "city": "Goldfield", + "state": "Minnesota", + "country": "Qatar", + "company": "Xelegyl", + "favoriteNumber": 0 + }, + { + "id": 91, + "name": "Crystal Kinney", + "city": "Nogal", + "state": "Georgia", + "country": "Kuwait", + "company": "Zork", + "favoriteNumber": 3 + }, + { + "id": 92, + "name": "Witt Colon", + "city": "Yorklyn", + "state": "Connecticut", + "country": "Singapore", + "company": "Techmania", + "favoriteNumber": 8 + }, + { + "id": 93, + "name": "Joyce Randolph", + "city": "Leland", + "state": "South Carolina", + "country": "Dominica", + "company": "Realmo", + "favoriteNumber": 2 + }, + { + "id": 94, + "name": "Ora Oneil", + "city": "Gilgo", + "state": "Missouri", + "country": "Bahamas", + "company": "Hinway", + "favoriteNumber": 7 + }, + { + "id": 95, + "name": "Hansen Rose", + "city": "Starks", + "state": "Maine", + "country": "Iran", + "company": "Virxo", + "favoriteNumber": 6 + }, + { + "id": 96, + "name": "Isabelle Rush", + "city": "Datil", + "state": "Wisconsin", + "country": "Switzerland", + "company": "Ecraze", + "favoriteNumber": 4 + }, + { + "id": 97, + "name": "Hoffman Crosby", + "city": "Trucksville", + "state": "Alabama", + "country": "Indonesia", + "company": "Multron", + "favoriteNumber": 10 + }, + { + "id": 98, + "name": "Louella Cotton", + "city": "Shelby", + "state": "Hawaii", + "country": "Tajikistan", + "company": "Supportal", + "favoriteNumber": 0 + }, + { + "id": 99, + "name": "Elvia Drake", + "city": "Albrightsville", + "state": "New Jersey", + "country": "Grenada", + "company": "Kiosk", + "favoriteNumber": 10 + }, + { + "id": 100, + "name": "Tyson Guerra", + "city": "Sutton", + "state": "Arizona", + "country": "Benin", + "company": "Dadabase", + "favoriteNumber": 4 + }, + { + "id": 101, + "name": "Marion Sloan", + "city": "Winchester", + "state": "Illinois", + "country": "Venezuela", + "company": "Exostream", + "favoriteNumber": 0 + }, + { + "id": 102, + "name": "Faulkner Diaz", + "city": "Logan", + "state": "Wyoming", + "country": "Monaco", + "company": "Oceanica", + "favoriteNumber": 10 + }, + { + "id": 103, + "name": "Penelope Price", + "city": "Alafaya", + "state": "Delaware", + "country": "Chile", + "company": "Nebulean", + "favoriteNumber": 10 + }, + { + "id": 104, + "name": "Kaitlin Glover", + "city": "Succasunna", + "state": "Kentucky", + "country": "Puerto Rico", + "company": "Orbean", + "favoriteNumber": 4 + }, + { + "id": 105, + "name": "Elena English", + "city": "Wedgewood", + "state": "New Mexico", + "country": "Algeria", + "company": "Kiggle", + "favoriteNumber": 0 + }, + { + "id": 106, + "name": "Clemons Sweeney", + "city": "Saranap", + "state": "Montana", + "country": "Ghana", + "company": "Konnect", + "favoriteNumber": 8 + }, + { + "id": 107, + "name": "Kelsey Blevins", + "city": "Vincent", + "state": "Rhode Island", + "country": "Albania", + "company": "Xymonk", + "favoriteNumber": 7 + }, + { + "id": 108, + "name": "Schroeder Craft", + "city": "Roosevelt", + "state": "North Carolina", + "country": "Satellite", + "company": "Isotrack", + "favoriteNumber": 8 + }, + { + "id": 109, + "name": "Hill Clark", + "city": "Elrama", + "state": "Maryland", + "country": "Slovakia", + "company": "Waterbaby", + "favoriteNumber": 0 + }, + { + "id": 110, + "name": "Glover Meyers", + "city": "Riviera", + "state": "Indiana", + "country": "Liberia", + "company": "Digigene", + "favoriteNumber": 0 + }, + { + "id": 111, + "name": "Lola Parrish", + "city": "Ellerslie", + "state": "Vermont", + "country": "Azerbaijan", + "company": "Myopium", + "favoriteNumber": 1 + }, + { + "id": 112, + "name": "Nora Rivers", + "city": "Belvoir", + "state": "Kansas", + "country": "Afghanistan", + "company": "Comtrek", + "favoriteNumber": 10 + }, + { + "id": 113, + "name": "Cohen Pacheco", + "city": "Bethpage", + "state": "Alaska", + "country": "Netherlands Antilles", + "company": "Accufarm", + "favoriteNumber": 7 + }, + { + "id": 114, + "name": "Diann Horn", + "city": "Derwood", + "state": "North Dakota", + "country": "Seychelles", + "company": "Synkgen", + "favoriteNumber": 3 + }, + { + "id": 115, + "name": "Amalia Nicholson", + "city": "Hendersonville", + "state": "Arkansas", + "country": "Lesotho", + "company": "Geekus", + "favoriteNumber": 2 + }, + { + "id": 116, + "name": "Mcgee Kane", + "city": "Dante", + "state": "New Hampshire", + "country": "Nigeria", + "company": "Kinetica", + "favoriteNumber": 9 + }, + { + "id": 117, + "name": "Shaffer Simpson", + "city": "Verdi", + "state": "Massachusetts", + "country": "Costa Rica", + "company": "Orbixtar", + "favoriteNumber": 0 + }, + { + "id": 118, + "name": "Lott Heath", + "city": "Castleton", + "state": "Utah", + "country": "Burkina Faso", + "company": "Reversus", + "favoriteNumber": 8 + }, + { + "id": 119, + "name": "Sasha Alvarez", + "city": "Foxworth", + "state": "Tennessee", + "country": "Montserrat", + "company": "Farmex", + "favoriteNumber": 9 + }, + { + "id": 120, + "name": "Sonja Rhodes", + "city": "Trona", + "state": "Pennsylvania", + "country": "Liechtenstein", + "company": "Pyramis", + "favoriteNumber": 2 + }, + { + "id": 121, + "name": "Rachel Elliott", + "city": "Hessville", + "state": "Louisiana", + "country": "Vietnam", + "company": "Centice", + "favoriteNumber": 0 + }, + { + "id": 122, + "name": "Elisa Justice", + "city": "Urie", + "state": "Idaho", + "country": "Senegal", + "company": "Dancerity", + "favoriteNumber": 2 + }, + { + "id": 123, + "name": "Velazquez Anderson", + "city": "Lowell", + "state": "Michigan", + "country": "Burundi", + "company": "Digial", + "favoriteNumber": 5 + }, + { + "id": 124, + "name": "Janet Ford", + "city": "Darlington", + "state": "Colorado", + "country": "Turkey", + "company": "Sportan", + "favoriteNumber": 10 + }, + { + "id": 125, + "name": "Simon Peterson", + "city": "Linwood", + "state": "Texas", + "country": "Kazakhstan", + "company": "Zytrac", + "favoriteNumber": 7 + }, + { + "id": 126, + "name": "Smith Baird", + "city": "Marne", + "state": "Nebraska", + "country": "Swaziland", + "company": "Genmy", + "favoriteNumber": 7 + }, + { + "id": 127, + "name": "Rogers Peters", + "city": "Tedrow", + "state": "West Virginia", + "country": "Spain", + "company": "Octocore", + "favoriteNumber": 1 + }, + { + "id": 128, + "name": "Bowers Ayers", + "city": "Matthews", + "state": "Oregon", + "country": "St Lucia", + "company": "Biotica", + "favoriteNumber": 6 + }, + { + "id": 129, + "name": "Paulette Delaney", + "city": "Riegelwood", + "state": "Oklahoma", + "country": "Guinea", + "company": "Netbook", + "favoriteNumber": 3 + }, + { + "id": 130, + "name": "Pat Klein", + "city": "Jacksonburg", + "state": "Ohio", + "country": "El Salvador", + "company": "Recrisys", + "favoriteNumber": 5 + }, + { + "id": 131, + "name": "Dena Rosa", + "city": "Hollymead", + "state": "Virginia", + "country": "Russia", + "company": "Sealoud", + "favoriteNumber": 6 + }, + { + "id": 132, + "name": "Rochelle Barnett", + "city": "Genoa", + "state": "Mississippi", + "country": "Kenya", + "company": "Ersum", + "favoriteNumber": 6 + }, + { + "id": 133, + "name": "Odom Schultz", + "city": "Blende", + "state": "South Dakota", + "country": "Papua New Guinea", + "company": "Elentrix", + "favoriteNumber": 0 + }, + { + "id": 134, + "name": "Anderson Franco", + "city": "Yardville", + "state": "Iowa", + "country": "Sweden", + "company": "Wazzu", + "favoriteNumber": 4 + }, + { + "id": 135, + "name": "Rebecca Wyatt", + "city": "Berwind", + "state": "Washington", + "country": "St Vincent", + "company": "Bristo", + "favoriteNumber": 2 + }, + { + "id": 136, + "name": "Dollie Hooper", + "city": "Richmond", + "state": "Florida", + "country": "Uruguay", + "company": "Vantage", + "favoriteNumber": 1 + }, + { + "id": 137, + "name": "Mathews Sharpe", + "city": "Glenshaw", + "state": "California", + "country": "Trinidad & Tobago", + "company": "Quilk", + "favoriteNumber": 4 + }, + { + "id": 138, + "name": "Debra Skinner", + "city": "Leming", + "state": "New York", + "country": "Saint Pierre & Miquelon", + "company": "Billmed", + "favoriteNumber": 1 + }, + { + "id": 139, + "name": "Cross Wells", + "city": "Caroline", + "state": "Minnesota", + "country": "Mauritania", + "company": "Strozen", + "favoriteNumber": 3 + }, + { + "id": 140, + "name": "Dodson Aguirre", + "city": "Nash", + "state": "Georgia", + "country": "Palestine", + "company": "Tripsch", + "favoriteNumber": 5 + }, + { + "id": 141, + "name": "Edna Copeland", + "city": "Harrison", + "state": "Connecticut", + "country": "Macedonia", + "company": "Flum", + "favoriteNumber": 10 + }, + { + "id": 142, + "name": "Dominguez Goodwin", + "city": "Condon", + "state": "South Carolina", + "country": "Laos", + "company": "Interloo", + "favoriteNumber": 0 + }, + { + "id": 143, + "name": "Fry Leach", + "city": "Advance", + "state": "Missouri", + "country": "Angola", + "company": "Recritube", + "favoriteNumber": 7 + }, + { + "id": 144, + "name": "Mann Malone", + "city": "Lumberton", + "state": "Maine", + "country": "India", + "company": "Xylar", + "favoriteNumber": 9 + }, + { + "id": 145, + "name": "Bridget Ayala", + "city": "Bellamy", + "state": "Wisconsin", + "country": "Cote D Ivoire", + "company": "Comvex", + "favoriteNumber": 7 + }, + { + "id": 146, + "name": "Blackwell Blanchard", + "city": "Ticonderoga", + "state": "Alabama", + "country": "Barbados", + "company": "Applideck", + "favoriteNumber": 7 + }, + { + "id": 147, + "name": "Maxine Irwin", + "city": "Longoria", + "state": "Hawaii", + "country": "Armenia", + "company": "Pearlesex", + "favoriteNumber": 10 + }, + { + "id": 148, + "name": "Laura Bryant", + "city": "Chicopee", + "state": "New Jersey", + "country": "Bosnia & Herzegovina", + "company": "Poshome", + "favoriteNumber": 0 + }, + { + "id": 149, + "name": "Zimmerman Little", + "city": "Rosewood", + "state": "Arizona", + "country": "Guatemala", + "company": "Boink", + "favoriteNumber": 4 + }, + { + "id": 150, + "name": "Barlow Reed", + "city": "Buxton", + "state": "Illinois", + "country": "Tanzania", + "company": "Premiant", + "favoriteNumber": 5 + }, + { + "id": 151, + "name": "Anita Briggs", + "city": "Laurelton", + "state": "Wyoming", + "country": "United Arab Emirates", + "company": "Codact", + "favoriteNumber": 3 + }, + { + "id": 152, + "name": "Ortiz Newton", + "city": "Blandburg", + "state": "Delaware", + "country": "Moldova", + "company": "Enersave", + "favoriteNumber": 7 + }, + { + "id": 153, + "name": "Cox Monroe", + "city": "Dupuyer", + "state": "Kentucky", + "country": "Taiwan", + "company": "Uneeq", + "favoriteNumber": 3 + }, + { + "id": 154, + "name": "Elinor Hughes", + "city": "Yukon", + "state": "New Mexico", + "country": "Bulgaria", + "company": "Bovis", + "favoriteNumber": 5 + }, + { + "id": 155, + "name": "Ronda Burks", + "city": "Ferney", + "state": "Montana", + "country": "Isle of Man", + "company": "Signity", + "favoriteNumber": 6 + }, + { + "id": 156, + "name": "Lourdes Walls", + "city": "Norwood", + "state": "Rhode Island", + "country": "Argentina", + "company": "Snacktion", + "favoriteNumber": 5 + }, + { + "id": 157, + "name": "Susana Mcintosh", + "city": "Manchester", + "state": "North Carolina", + "country": "Israel", + "company": "Teraprene", + "favoriteNumber": 5 + }, + { + "id": 158, + "name": "Alfreda Henry", + "city": "Wilsonia", + "state": "Maryland", + "country": "Bhutan", + "company": "Coash", + "favoriteNumber": 3 + }, + { + "id": 159, + "name": "Tiffany Chaney", + "city": "Carrsville", + "state": "Indiana", + "country": "Morocco", + "company": "Cinaster", + "favoriteNumber": 7 + }, + { + "id": 160, + "name": "Morton Edwards", + "city": "Barstow", + "state": "Vermont", + "country": "Hong Kong", + "company": "Ultrasure", + "favoriteNumber": 10 + }, + { + "id": 161, + "name": "Marcy Serrano", + "city": "Idamay", + "state": "Kansas", + "country": "Finland", + "company": "Isbol", + "favoriteNumber": 3 + }, + { + "id": 162, + "name": "Wendi Gutierrez", + "city": "Camas", + "state": "Alaska", + "country": "Andorra", + "company": "Turnling", + "favoriteNumber": 6 + }, + { + "id": 163, + "name": "Miriam Gates", + "city": "Helen", + "state": "North Dakota", + "country": "Djibouti", + "company": "Undertap", + "favoriteNumber": 8 + }, + { + "id": 164, + "name": "Adrienne Horne", + "city": "Snyderville", + "state": "Arkansas", + "country": "Gambia", + "company": "Olympix", + "favoriteNumber": 2 + }, + { + "id": 165, + "name": "Steele Morales", + "city": "Kenvil", + "state": "New Hampshire", + "country": "Macau", + "company": "Animalia", + "favoriteNumber": 3 + }, + { + "id": 166, + "name": "Ericka Morgan", + "city": "Leroy", + "state": "Massachusetts", + "country": "Kyrgyz Republic", + "company": "Opticall", + "favoriteNumber": 5 + }, + { + "id": 167, + "name": "Deborah Davenport", + "city": "Albany", + "state": "Utah", + "country": "Thailand", + "company": "Vetron", + "favoriteNumber": 10 + }, + { + "id": 168, + "name": "Tameka Mcneil", + "city": "Frierson", + "state": "Tennessee", + "country": "St. Lucia", + "company": "Martgo", + "favoriteNumber": 1 + }, + { + "id": 169, + "name": "Jewell Shields", + "city": "Bannock", + "state": "Pennsylvania", + "country": "Maldives", + "company": "Lotron", + "favoriteNumber": 8 + }, + { + "id": 170, + "name": "Crawford Fox", + "city": "Nicholson", + "state": "Louisiana", + "country": "Rwanda", + "company": "Progenex", + "favoriteNumber": 8 + }, + { + "id": 171, + "name": "Vaughan Tanner", + "city": "Cuylerville", + "state": "Idaho", + "country": "Jamaica", + "company": "Zeam", + "favoriteNumber": 3 + }, + { + "id": 172, + "name": "Shauna Wagner", + "city": "Disautel", + "state": "Michigan", + "country": "China", + "company": "Isologia", + "favoriteNumber": 6 + }, + { + "id": 173, + "name": "Meagan Hines", + "city": "Whitmer", + "state": "Colorado", + "country": "Jordan", + "company": "Grainspot", + "favoriteNumber": 8 + }, + { + "id": 174, + "name": "Palmer Bender", + "city": "Beechmont", + "state": "Texas", + "country": "Japan", + "company": "Vurbo", + "favoriteNumber": 10 + }, + { + "id": 175, + "name": "Amanda Buck", + "city": "Elfrida", + "state": "Nebraska", + "country": "Latvia", + "company": "Frosnex", + "favoriteNumber": 5 + }, + { + "id": 176, + "name": "Kristin Cleveland", + "city": "Richville", + "state": "West Virginia", + "country": "Lithuania", + "company": "Honotron", + "favoriteNumber": 2 + }, + { + "id": 177, + "name": "Harrell Vaughan", + "city": "Munjor", + "state": "Oregon", + "country": "Anguilla", + "company": "Orbalix", + "favoriteNumber": 9 + }, + { + "id": 178, + "name": "Stanley Webb", + "city": "Harleigh", + "state": "Oklahoma", + "country": "Mali", + "company": "Motovate", + "favoriteNumber": 6 + }, + { + "id": 179, + "name": "Briana Mitchell", + "city": "Kansas", + "state": "Ohio", + "country": "Libya", + "company": "Zillatide", + "favoriteNumber": 10 + }, + { + "id": 180, + "name": "Lillian Osborne", + "city": "Eastmont", + "state": "Virginia", + "country": "Belize", + "company": "Circum", + "favoriteNumber": 6 + }, + { + "id": 181, + "name": "Hughes Morse", + "city": "Herlong", + "state": "Mississippi", + "country": "French West Indies", + "company": "Endipine", + "favoriteNumber": 0 + }, + { + "id": 182, + "name": "Elise Whitehead", + "city": "Hailesboro", + "state": "South Dakota", + "country": "Saudi Arabia", + "company": "Geekmosis", + "favoriteNumber": 10 + }, + { + "id": 183, + "name": "Alyce Chavez", + "city": "Bendon", + "state": "Iowa", + "country": "Portugal", + "company": "Dognost", + "favoriteNumber": 8 + }, + { + "id": 184, + "name": "Goff Walker", + "city": "Sultana", + "state": "Washington", + "country": "Germany", + "company": "Uncorp", + "favoriteNumber": 4 + }, + { + "id": 185, + "name": "Brennan Melton", + "city": "Baker", + "state": "Florida", + "country": "Austria", + "company": "Thredz", + "favoriteNumber": 0 + }, + { + "id": 186, + "name": "Toni Brennan", + "city": "Newry", + "state": "California", + "country": "Serbia", + "company": "Bitendrex", + "favoriteNumber": 0 + }, + { + "id": 187, + "name": "Mcmillan Lane", + "city": "Thornport", + "state": "New York", + "country": "Panama", + "company": "Kengen", + "favoriteNumber": 2 + }, + { + "id": 188, + "name": "Yang Trujillo", + "city": "Falmouth", + "state": "Minnesota", + "country": "Paraguay", + "company": "Vitricomp", + "favoriteNumber": 7 + }, + { + "id": 189, + "name": "Osborn Love", + "city": "Rehrersburg", + "state": "Georgia", + "country": "Peru", + "company": "Newcube", + "favoriteNumber": 7 + }, + { + "id": 190, + "name": "Randolph Giles", + "city": "Sandston", + "state": "Connecticut", + "country": "Niger", + "company": "Manglo", + "favoriteNumber": 6 + }, + { + "id": 191, + "name": "Alison Eaton", + "city": "Wauhillau", + "state": "South Carolina", + "country": "St Kitts & Nevis", + "company": "Buzzmaker", + "favoriteNumber": 7 + }, + { + "id": 192, + "name": "Frankie Pollard", + "city": "Salix", + "state": "Missouri", + "country": "Uganda", + "company": "Jasper", + "favoriteNumber": 3 + }, + { + "id": 193, + "name": "Shields Cole", + "city": "Olney", + "state": "Maine", + "country": "British Virgin Islands", + "company": "Anivet", + "favoriteNumber": 3 + }, + { + "id": 194, + "name": "Frieda Wilkins", + "city": "Darrtown", + "state": "Wisconsin", + "country": "Gibraltar", + "company": "Elemantra", + "favoriteNumber": 6 + }, + { + "id": 195, + "name": "Parker Meyer", + "city": "Deseret", + "state": "Alabama", + "country": "Bermuda", + "company": "Cablam", + "favoriteNumber": 6 + }, + { + "id": 196, + "name": "Sharpe Blankenship", + "city": "Sparkill", + "state": "Hawaii", + "country": "Jersey", + "company": "Affluex", + "favoriteNumber": 0 + }, + { + "id": 197, + "name": "Fletcher Pope", + "city": "Libertytown", + "state": "New Jersey", + "country": "Bahrain", + "company": "Veraq", + "favoriteNumber": 6 + }, + { + "id": 198, + "name": "Brittany Holland", + "city": "Stonybrook", + "state": "Arizona", + "country": "Cook Islands", + "company": "Menbrain", + "favoriteNumber": 6 + }, + { + "id": 199, + "name": "Tammi Good", + "city": "Gwynn", + "state": "Illinois", + "country": "Denmark", + "company": "Kangle", + "favoriteNumber": 5 + }, + { + "id": 200, + "name": "Durham Valentine", + "city": "Dodge", + "state": "Wyoming", + "country": "Bolivia", + "company": "Amtas", + "favoriteNumber": 9 + }, + { + "id": 201, + "name": "Gina Savage", + "city": "Camptown", + "state": "Delaware", + "country": "San Marino", + "company": "Golistic", + "favoriteNumber": 8 + }, + { + "id": 202, + "name": "Faith Crane", + "city": "Kingstowne", + "state": "Kentucky", + "country": "Guyana", + "company": "Providco", + "favoriteNumber": 1 + }, + { + "id": 203, + "name": "Mullins Hewitt", + "city": "Courtland", + "state": "New Mexico", + "country": "Colombia", + "company": "Paprikut", + "favoriteNumber": 0 + }, + { + "id": 204, + "name": "Kemp Barber", + "city": "Morriston", + "state": "Montana", + "country": "United Kingdom", + "company": "Geekfarm", + "favoriteNumber": 3 + }, + { + "id": 205, + "name": "Sheppard Shaw", + "city": "Vandiver", + "state": "Rhode Island", + "country": "Madagascar", + "company": "Fossiel", + "favoriteNumber": 9 + }, + { + "id": 206, + "name": "Keith Bradshaw", + "city": "Mulberry", + "state": "North Carolina", + "country": "Ukraine", + "company": "Comtent", + "favoriteNumber": 9 + }, + { + "id": 207, + "name": "Dianne Conley", + "city": "Tonopah", + "state": "Maryland", + "country": "New Zealand", + "company": "Joviold", + "favoriteNumber": 5 + }, + { + "id": 208, + "name": "Love Griffin", + "city": "Day", + "state": "Indiana", + "country": "Ecuador", + "company": "Vortexaco", + "favoriteNumber": 6 + }, + { + "id": 209, + "name": "Melody Delacruz", + "city": "Hanover", + "state": "Vermont", + "country": "Virgin Islands (US)", + "company": "Pyramia", + "favoriteNumber": 6 + }, + { + "id": 210, + "name": "Patsy Kramer", + "city": "Southmont", + "state": "Kansas", + "country": "Sri Lanka", + "company": "Bedder", + "favoriteNumber": 8 + }, + { + "id": 211, + "name": "Becky Richard", + "city": "Crenshaw", + "state": "Alaska", + "country": "Croatia", + "company": "Polarium", + "favoriteNumber": 1 + }, + { + "id": 212, + "name": "Leon Rivera", + "city": "Gibbsville", + "state": "North Dakota", + "country": "Turks & Caicos", + "company": "Micronaut", + "favoriteNumber": 3 + }, + { + "id": 213, + "name": "Simpson Randall", + "city": "Cetronia", + "state": "Arkansas", + "country": "Cambodia", + "company": "Intergeek", + "favoriteNumber": 2 + }, + { + "id": 214, + "name": "Daugherty Duke", + "city": "Levant", + "state": "New Hampshire", + "country": "Namibia", + "company": "Nutralab", + "favoriteNumber": 3 + }, + { + "id": 215, + "name": "Payne Morton", + "city": "Jamestown", + "state": "Massachusetts", + "country": "Cape Verde", + "company": "Insource", + "favoriteNumber": 7 + }, + { + "id": 216, + "name": "Perkins Leblanc", + "city": "Boyd", + "state": "Utah", + "country": "Brunei", + "company": "Dognosis", + "favoriteNumber": 1 + }, + { + "id": 217, + "name": "Phillips Douglas", + "city": "Wikieup", + "state": "Tennessee", + "country": "Lebanon", + "company": "Ziore", + "favoriteNumber": 9 + }, + { + "id": 218, + "name": "Graves Stark", + "city": "Barrelville", + "state": "Pennsylvania", + "country": "Timor L'Este", + "company": "Exovent", + "favoriteNumber": 8 + }, + { + "id": 219, + "name": "Munoz Johns", + "city": "Wyano", + "state": "Louisiana", + "country": "Samoa", + "company": "Escenta", + "favoriteNumber": 10 + }, + { + "id": 220, + "name": "Myra Salazar", + "city": "Gorst", + "state": "Idaho", + "country": "Greece", + "company": "Acrodance", + "favoriteNumber": 10 + }, + { + "id": 221, + "name": "Flynn Miranda", + "city": "Movico", + "state": "Michigan", + "country": "Greenland", + "company": "Artiq", + "favoriteNumber": 6 + }, + { + "id": 222, + "name": "Eloise Barr", + "city": "Greer", + "state": "Colorado", + "country": "Sierra Leone", + "company": "Insuresys", + "favoriteNumber": 5 + }, + { + "id": 223, + "name": "Harrington Daniels", + "city": "Dawn", + "state": "Texas", + "country": "Tonga", + "company": "Pharmex", + "favoriteNumber": 3 + }, + { + "id": 224, + "name": "Lester Carey", + "city": "Keller", + "state": "Nebraska", + "country": "Nepal", + "company": "Melbacor", + "favoriteNumber": 4 + }, + { + "id": 225, + "name": "Malinda Pittman", + "city": "Wyoming", + "state": "West Virginia", + "country": "Falkland Islands", + "company": "Rodeocean", + "favoriteNumber": 1 + }, + { + "id": 226, + "name": "Crane Smith", + "city": "Hoagland", + "state": "Oregon", + "country": "Syria", + "company": "Eclipsent", + "favoriteNumber": 8 + }, + { + "id": 227, + "name": "Ellison Underwood", + "city": "Neahkahnie", + "state": "Oklahoma", + "country": "Suriname", + "company": "Visualix", + "favoriteNumber": 4 + }, + { + "id": 228, + "name": "Shelby Hardy", + "city": "Bascom", + "state": "Ohio", + "country": "Malta", + "company": "Genekom", + "favoriteNumber": 8 + }, + { + "id": 229, + "name": "Sheena Maynard", + "city": "Morningside", + "state": "Virginia", + "country": "Dominican Republic", + "company": "Zillar", + "favoriteNumber": 10 + }, + { + "id": 230, + "name": "Tamera Roman", + "city": "Freelandville", + "state": "Mississippi", + "country": "Honduras", + "company": "Comtract", + "favoriteNumber": 6 + }, + { + "id": 231, + "name": "Juliette Hammond", + "city": "Lindcove", + "state": "South Dakota", + "country": "Yemen", + "company": "Coriander", + "favoriteNumber": 5 + }, + { + "id": 232, + "name": "Dean Holden", + "city": "Brantleyville", + "state": "Iowa", + "country": "Aruba", + "company": "Plexia", + "favoriteNumber": 5 + }, + { + "id": 233, + "name": "Whitfield Meadows", + "city": "Fedora", + "state": "Washington", + "country": "Egypt", + "company": "Isosure", + "favoriteNumber": 1 + }, + { + "id": 234, + "name": "Wiley Kelley", + "city": "Torboy", + "state": "Florida", + "country": "Malawi", + "company": "Zilladyne", + "favoriteNumber": 5 + }, + { + "id": 235, + "name": "Sherry Scott", + "city": "Garfield", + "state": "California", + "country": "Slovenia", + "company": "Otherway", + "favoriteNumber": 5 + }, + { + "id": 236, + "name": "Aline Sosa", + "city": "Martinez", + "state": "New York", + "country": "Australia", + "company": "Comstruct", + "favoriteNumber": 4 + }, + { + "id": 237, + "name": "Leta Rice", + "city": "Utting", + "state": "Minnesota", + "country": "Iraq", + "company": "Jumpstack", + "favoriteNumber": 8 + }, + { + "id": 238, + "name": "Ford Ingram", + "city": "Lafferty", + "state": "Georgia", + "country": "Hungary", + "company": "Sarasonic", + "favoriteNumber": 3 + }, + { + "id": 239, + "name": "Chan David", + "city": "Collins", + "state": "Connecticut", + "country": "Mongolia", + "company": "Velos", + "favoriteNumber": 6 + }, + { + "id": 240, + "name": "Jeanne Murray", + "city": "Carlos", + "state": "South Carolina", + "country": "Ethiopia", + "company": "Equitox", + "favoriteNumber": 6 + }, + { + "id": 241, + "name": "Fernandez Dean", + "city": "Wintersburg", + "state": "Missouri", + "country": "Bangladesh", + "company": "Orboid", + "favoriteNumber": 5 + }, + { + "id": 242, + "name": "Jordan Cox", + "city": "Orin", + "state": "Maine", + "country": "Sudan", + "company": "Roboid", + "favoriteNumber": 6 + }, + { + "id": 243, + "name": "Catherine Harper", + "city": "Bedias", + "state": "Wisconsin", + "country": "Zambia", + "company": "Photobin", + "favoriteNumber": 2 + }, + { + "id": 244, + "name": "Suarez Kelly", + "city": "Cressey", + "state": "Alabama", + "country": "Cameroon", + "company": "Xurban", + "favoriteNumber": 7 + }, + { + "id": 245, + "name": "Henderson Mcdonald", + "city": "Adamstown", + "state": "Hawaii", + "country": "Mexico", + "company": "Skinserve", + "favoriteNumber": 1 + }, + { + "id": 246, + "name": "Hardy Gibbs", + "city": "Chical", + "state": "New Jersey", + "country": "Zimbabwe", + "company": "Limage", + "favoriteNumber": 6 + }, + { + "id": 247, + "name": "Kimberley Yang", + "city": "Kenmar", + "state": "Arizona", + "country": "Tunisia", + "company": "Exotechno", + "favoriteNumber": 9 + }, + { + "id": 248, + "name": "Kristie Gilmore", + "city": "Fairview", + "state": "Illinois", + "country": "Italy", + "company": "Equicom", + "favoriteNumber": 4 + }, + { + "id": 249, + "name": "Jewel Hansen", + "city": "Worton", + "state": "Wyoming", + "country": "South Korea", + "company": "Sulfax", + "favoriteNumber": 6 + }, + { + "id": 250, + "name": "Cheryl Carter", + "city": "Caron", + "state": "Delaware", + "country": "Mauritius", + "company": "Netropic", + "favoriteNumber": 9 + }, + { + "id": 251, + "name": "Keisha Snider", + "city": "Waterloo", + "state": "Kentucky", + "country": "Romania", + "company": "Besto", + "favoriteNumber": 6 + }, + { + "id": 252, + "name": "Minnie Michael", + "city": "Cascades", + "state": "New Mexico", + "country": "South Africa", + "company": "Kidgrease", + "favoriteNumber": 10 + }, + { + "id": 253, + "name": "Sandy Mccullough", + "city": "Remington", + "state": "Montana", + "country": "Malaysia", + "company": "Zosis", + "favoriteNumber": 3 + }, + { + "id": 254, + "name": "Cervantes Maddox", + "city": "Sisquoc", + "state": "Rhode Island", + "country": "Oman", + "company": "Extremo", + "favoriteNumber": 6 + }, + { + "id": 255, + "name": "Sophia Logan", + "city": "Winfred", + "state": "North Carolina", + "country": "Ireland", + "company": "Enaut", + "favoriteNumber": 4 + }, + { + "id": 256, + "name": "Bertha Watson", + "city": "Caroleen", + "state": "Maryland", + "country": "France", + "company": "Musix", + "favoriteNumber": 9 + }, + { + "id": 257, + "name": "Emily Wilson", + "city": "Fairacres", + "state": "Indiana", + "country": "Iceland", + "company": "Comvene", + "favoriteNumber": 8 + }, + { + "id": 258, + "name": "Winters Petersen", + "city": "Wildwood", + "state": "Vermont", + "country": "Equatorial Guinea", + "company": "Webiotic", + "favoriteNumber": 0 + }, + { + "id": 259, + "name": "Chambers Finch", + "city": "Topaz", + "state": "Kansas", + "country": "Luxembourg", + "company": "Sultraxin", + "favoriteNumber": 5 + }, + { + "id": 260, + "name": "Byrd Mills", + "city": "Cloverdale", + "state": "Alaska", + "country": "Cruise Ship", + "company": "Accel", + "favoriteNumber": 2 + }, + { + "id": 261, + "name": "Gross Jacobs", + "city": "Duryea", + "state": "North Dakota", + "country": "Belarus", + "company": "Insuron", + "favoriteNumber": 5 + }, + { + "id": 262, + "name": "Jackson Sherman", + "city": "Waterview", + "state": "Arkansas", + "country": "Antigua & Barbuda", + "company": "Viagreat", + "favoriteNumber": 2 + }, + { + "id": 263, + "name": "Rhodes Boyer", + "city": "Enlow", + "state": "New Hampshire", + "country": "Chad", + "company": "Conferia", + "favoriteNumber": 4 + }, + { + "id": 264, + "name": "Campbell Rodgers", + "city": "Cumminsville", + "state": "Massachusetts", + "country": "Reunion", + "company": "Frolix", + "favoriteNumber": 10 + }, + { + "id": 265, + "name": "Bryant Sawyer", + "city": "Guilford", + "state": "Utah", + "country": "Gabon", + "company": "Rooforia", + "favoriteNumber": 10 + }, + { + "id": 266, + "name": "Joan Browning", + "city": "Elizaville", + "state": "Tennessee", + "country": "Fiji", + "company": "Kneedles", + "favoriteNumber": 6 + }, + { + "id": 267, + "name": "Jennie Mcintyre", + "city": "Draper", + "state": "Pennsylvania", + "country": "New Caledonia", + "company": "Isostream", + "favoriteNumber": 4 + }, + { + "id": 268, + "name": "Merle Jones", + "city": "Caspar", + "state": "Louisiana", + "country": "Haiti", + "company": "Tubesys", + "favoriteNumber": 4 + }, + { + "id": 269, + "name": "Ortega Burgess", + "city": "Thermal", + "state": "Idaho", + "country": "Mozambique", + "company": "Lovepad", + "favoriteNumber": 9 + }, + { + "id": 270, + "name": "Deanna Grimes", + "city": "Flintville", + "state": "Michigan", + "country": "Pakistan", + "company": "Omatom", + "favoriteNumber": 10 + }, + { + "id": 271, + "name": "Jeanie Ochoa", + "city": "Ruckersville", + "state": "Colorado", + "country": "Cayman Islands", + "company": "Momentia", + "favoriteNumber": 8 + }, + { + "id": 272, + "name": "Morrow Valencia", + "city": "Roberts", + "state": "Texas", + "country": "Guam", + "company": "Permadyne", + "favoriteNumber": 4 + }, + { + "id": 273, + "name": "Hull Wade", + "city": "Monument", + "state": "Nebraska", + "country": "Cyprus", + "company": "Indexia", + "favoriteNumber": 10 + }, + { + "id": 274, + "name": "Blanca Sheppard", + "city": "Wadsworth", + "state": "West Virginia", + "country": "Nicaragua", + "company": "Gogol", + "favoriteNumber": 7 + }, + { + "id": 275, + "name": "Stella Luna", + "city": "Dubois", + "state": "Oregon", + "country": "Czech Republic", + "company": "Intrawear", + "favoriteNumber": 1 + } +]; + +export var fakeData; + diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs new file mode 100755 index 000000000000..46ddd6035438 --- /dev/null +++ b/samples/react/ReactGrid/Startup.cs @@ -0,0 +1,68 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; +using Microsoft.Dnx.Runtime; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; + +namespace ReactExample +{ + public class Startup + { + public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) + { + // Setup configuration sources. + var builder = new ConfigurationBuilder() + .SetBasePath(appEnv.ApplicationBasePath) + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; set; } + + // This method gets called by the runtime. + public void ConfigureServices(IServiceCollection services) + { + // Add MVC services to the services container. + services.AddMvc(); + } + + // Configure is called after ConfigureServices is called. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.AddConsole(); + loggerFactory.AddDebug(); + + // Configure the HTTP request pipeline. + + // Add the platform handler to the request pipeline. + app.UseIISPlatformHandler(); + + // Add the following to the request pipeline only in development environment. + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + // Add Error handling middleware which catches all application specific errors and + // send the request to the following path or controller action. + app.UseExceptionHandler("/Home/Error"); + } + + // Add static files to the request pipeline. + app.UseStaticFiles(); + + // Add MVC to the request pipeline. + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{pageIndex?}", + defaults: new { controller="Home", action = "Index" }); + }); + } + } +} diff --git a/samples/react/ReactGrid/Views/Home/Index.cshtml b/samples/react/ReactGrid/Views/Home/Index.cshtml new file mode 100755 index 000000000000..bef6868f48af --- /dev/null +++ b/samples/react/ReactGrid/Views/Home/Index.cshtml @@ -0,0 +1,5 @@ +
@Html.Raw(ViewData["ReactOutput"])
+ +@section scripts { + +} diff --git a/samples/react/ReactGrid/Views/Shared/Error.cshtml b/samples/react/ReactGrid/Views/Shared/Error.cshtml new file mode 100755 index 000000000000..a288cb0581f8 --- /dev/null +++ b/samples/react/ReactGrid/Views/Shared/Error.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

diff --git a/samples/react/ReactGrid/Views/Shared/_Layout.cshtml b/samples/react/ReactGrid/Views/Shared/_Layout.cshtml new file mode 100755 index 000000000000..4e83db4cc892 --- /dev/null +++ b/samples/react/ReactGrid/Views/Shared/_Layout.cshtml @@ -0,0 +1,12 @@ + + + + + ReactExample + + + + @RenderBody() + @RenderSection("scripts", required: false) + + diff --git a/samples/react/ReactGrid/Views/_ViewImports.cshtml b/samples/react/ReactGrid/Views/_ViewImports.cshtml new file mode 100755 index 000000000000..7839f6c9ff9b --- /dev/null +++ b/samples/react/ReactGrid/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using ReactExample +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" diff --git a/samples/react/ReactGrid/Views/_ViewStart.cshtml b/samples/react/ReactGrid/Views/_ViewStart.cshtml new file mode 100755 index 000000000000..66b5da255ace --- /dev/null +++ b/samples/react/ReactGrid/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/samples/react/ReactGrid/appsettings.json b/samples/react/ReactGrid/appsettings.json new file mode 100755 index 000000000000..0967ef424bce --- /dev/null +++ b/samples/react/ReactGrid/appsettings.json @@ -0,0 +1 @@ +{} diff --git a/samples/react/ReactGrid/jsconfig.json b/samples/react/ReactGrid/jsconfig.json new file mode 100644 index 000000000000..875bb90cd697 --- /dev/null +++ b/samples/react/ReactGrid/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs" + } +} diff --git a/samples/react/ReactGrid/package.json b/samples/react/ReactGrid/package.json new file mode 100644 index 000000000000..86bf49d22341 --- /dev/null +++ b/samples/react/ReactGrid/package.json @@ -0,0 +1,25 @@ +{ + "name": "ReactExample", + "version": "0.0.0", + "dependencies": { + "babel-core": "^5.8.29", + "body-parser": "^1.14.1", + "bootstrap": "^3.3.5", + "express": "^4.13.3", + "griddle-react": "^0.2.14", + "history": "^1.12.6", + "react": "^0.14.0", + "react-dom": "^0.14.0", + "react-router": "^1.0.0-rc3", + "underscore": "^1.8.3" + }, + "devDependencies": { + "babel-loader": "^5.3.2", + "css-loader": "^0.21.0", + "extract-text-webpack-plugin": "^0.8.2", + "file-loader": "^0.8.4", + "style-loader": "^0.13.0", + "url-loader": "^0.5.6", + "webpack": "^1.12.2" + } +} diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json new file mode 100755 index 000000000000..5a346c885f0f --- /dev/null +++ b/samples/react/ReactGrid/project.json @@ -0,0 +1,45 @@ +{ + "webroot": "wwwroot", + "version": "1.0.0-*", + "tooling": { + "defaultNamespace": "ReactExample" + }, + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", + "Microsoft.AspNet.Mvc": "6.0.0-beta8", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", + "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", + "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", + "Microsoft.Framework.Logging": "1.0.0-beta8", + "Microsoft.Framework.Logging.Console": "1.0.0-beta8", + "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", + "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha1" + }, + "commands": { + "web": "Microsoft.AspNet.Server.Kestrel" + }, + "frameworks": { + "dnx451": {}, + "dnxcore50": {} + }, + "exclude": [ + "wwwroot", + "node_modules", + "bower_components" + ], + "publishExclude": [ + "node_modules", + "bower_components", + "**.xproj", + "**.user", + "**.vspscc" + ], + "scripts": { + "prepublish": [ + "npm install" + ] + } +} \ No newline at end of file diff --git a/samples/react/ReactGrid/webpack.config.js b/samples/react/ReactGrid/webpack.config.js new file mode 100644 index 000000000000..02884bc6dce6 --- /dev/null +++ b/samples/react/ReactGrid/webpack.config.js @@ -0,0 +1,19 @@ +var ExtractTextPlugin = require('extract-text-webpack-plugin'); + +module.exports = { + entry: './ReactApp/boot-client.jsx', + output: { + path: './wwwroot', + filename: 'bundle.js' + }, + module: { + loaders: [ + { test: /\.jsx?$/, loader: 'babel-loader' }, + { test: /\.css$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader') }, + { test: /\.(png|woff|woff2|eot|ttf|svg)$/, loader: 'url-loader?limit=100000' } + ] + }, + plugins: [ + new ExtractTextPlugin('main.css') + ] +}; diff --git a/samples/react/ReactGrid/wwwroot/favicon.ico b/samples/react/ReactGrid/wwwroot/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 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 + + + + + + + + From 60d77e7b924240a9d0fabe4044377511bbd6af4a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 11:15:34 -0800 Subject: [PATCH 0005/1585] Add ES2015 runtime transpilation sample --- samples/misc/ES2015Transpilation/.gitignore | 1 + .../Controllers/HomeController.cs | 18 ++++ .../Controllers/ScriptController.cs | 19 +++++ samples/misc/ES2015Transpilation/Startup.cs | 78 ++++++++++++++++++ .../Views/Home/Index.cshtml | 6 ++ .../Views/Shared/Error.cshtml | 6 ++ .../Views/Shared/_Layout.cshtml | 11 +++ .../Views/_ViewImports.cshtml | 2 + .../Views/_ViewStart.cshtml | 3 + .../misc/ES2015Transpilation/appsettings.json | 1 + .../misc/ES2015Transpilation/jsconfig.json | 6 ++ samples/misc/ES2015Transpilation/package.json | 9 ++ samples/misc/ES2015Transpilation/project.json | 45 ++++++++++ .../misc/ES2015Transpilation/transpilation.js | 6 ++ .../ES2015Transpilation/wwwroot/favicon.ico | Bin 0 -> 32038 bytes .../wwwroot/js/greeting.js | 5 ++ .../ES2015Transpilation/wwwroot/js/main.js | 3 + .../ES2015Transpilation/wwwroot/lib/system.js | 5 ++ .../ES2015Transpilation/wwwroot/web.config | 9 ++ 19 files changed, 233 insertions(+) create mode 100644 samples/misc/ES2015Transpilation/.gitignore create mode 100755 samples/misc/ES2015Transpilation/Controllers/HomeController.cs create mode 100644 samples/misc/ES2015Transpilation/Controllers/ScriptController.cs create mode 100755 samples/misc/ES2015Transpilation/Startup.cs create mode 100755 samples/misc/ES2015Transpilation/Views/Home/Index.cshtml create mode 100755 samples/misc/ES2015Transpilation/Views/Shared/Error.cshtml create mode 100755 samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml create mode 100755 samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml create mode 100755 samples/misc/ES2015Transpilation/Views/_ViewStart.cshtml create mode 100755 samples/misc/ES2015Transpilation/appsettings.json create mode 100644 samples/misc/ES2015Transpilation/jsconfig.json create mode 100644 samples/misc/ES2015Transpilation/package.json create mode 100755 samples/misc/ES2015Transpilation/project.json create mode 100644 samples/misc/ES2015Transpilation/transpilation.js create mode 100755 samples/misc/ES2015Transpilation/wwwroot/favicon.ico create mode 100644 samples/misc/ES2015Transpilation/wwwroot/js/greeting.js create mode 100644 samples/misc/ES2015Transpilation/wwwroot/js/main.js create mode 100644 samples/misc/ES2015Transpilation/wwwroot/lib/system.js create mode 100644 samples/misc/ES2015Transpilation/wwwroot/web.config diff --git a/samples/misc/ES2015Transpilation/.gitignore b/samples/misc/ES2015Transpilation/.gitignore new file mode 100644 index 000000000000..2ccbe4656c60 --- /dev/null +++ b/samples/misc/ES2015Transpilation/.gitignore @@ -0,0 +1 @@ +/node_modules/ diff --git a/samples/misc/ES2015Transpilation/Controllers/HomeController.cs b/samples/misc/ES2015Transpilation/Controllers/HomeController.cs new file mode 100755 index 000000000000..9d9dd3e6b5b4 --- /dev/null +++ b/samples/misc/ES2015Transpilation/Controllers/HomeController.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace ES2015Example.Controllers +{ + public class HomeController : Controller + { + public async Task Index(int pageIndex) + { + return View(); + } + + public IActionResult Error() + { + return View("~/Views/Shared/Error.cshtml"); + } + } +} diff --git a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs new file mode 100644 index 000000000000..62f0878a5efc --- /dev/null +++ b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.NodeServices; + +namespace ES2015Example.Controllers +{ + public class ScriptController : Controller + { + private static NodeInstance nodeInstance = new NodeInstance(); + + public async Task Transpile(string filename) + { + // TODO: Don't hard-code wwwroot; use proper path conversions + var fileContents = System.IO.File.ReadAllText("wwwroot/" + filename); + var transpiledResult = await nodeInstance.Invoke("transpilation.js", fileContents); + return Content(transpiledResult, "application/javascript"); + } + } +} diff --git a/samples/misc/ES2015Transpilation/Startup.cs b/samples/misc/ES2015Transpilation/Startup.cs new file mode 100755 index 000000000000..b3584db95a71 --- /dev/null +++ b/samples/misc/ES2015Transpilation/Startup.cs @@ -0,0 +1,78 @@ +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Routing.Template; +using Microsoft.Dnx.Runtime; +using Microsoft.Framework.Configuration; +using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.Logging; + +namespace ES2015Example +{ + public class Startup + { + public Startup(IHostingEnvironment env, IApplicationEnvironment appEnv) + { + // Setup configuration sources. + var builder = new ConfigurationBuilder() + .SetBasePath(appEnv.ApplicationBasePath) + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables(); + Configuration = builder.Build(); + } + + public IConfigurationRoot Configuration { get; set; } + + // This method gets called by the runtime. + public void ConfigureServices(IServiceCollection services) + { + // Add MVC services to the services container. + services.AddMvc(); + } + + // Configure is called after ConfigureServices is called. + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + { + loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.AddConsole(); + loggerFactory.AddDebug(); + + // Configure the HTTP request pipeline. + + // Add the platform handler to the request pipeline. + app.UseIISPlatformHandler(); + + // Add the following to the request pipeline only in development environment. + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + // Add Error handling middleware which catches all application specific errors and + // send the request to the following path or controller action. + app.UseExceptionHandler("/Home/Error"); + } + + app.UseMvc(routes => { + routes.MapRoute( + name: "Script", + template: "{*filename}", + defaults: new { controller="Script", action="Transpile" }, + constraints: new { filename = @"js/(.*?)\.js" } + ); + }); + + // Add static files to the request pipeline. + app.UseStaticFiles(); + + // Add MVC to the request pipeline. + app.UseMvc(routes => + { + routes.MapRoute( + name: "default", + template: "{controller}/{action?}/{id?}", + defaults: new { controller="Home", action = "Index" }); + }); + } + } +} diff --git a/samples/misc/ES2015Transpilation/Views/Home/Index.cshtml b/samples/misc/ES2015Transpilation/Views/Home/Index.cshtml new file mode 100755 index 000000000000..5b5e2f220768 --- /dev/null +++ b/samples/misc/ES2015Transpilation/Views/Home/Index.cshtml @@ -0,0 +1,6 @@ +Hello + +@section scripts { + + +} diff --git a/samples/misc/ES2015Transpilation/Views/Shared/Error.cshtml b/samples/misc/ES2015Transpilation/Views/Shared/Error.cshtml new file mode 100755 index 000000000000..a288cb0581f8 --- /dev/null +++ b/samples/misc/ES2015Transpilation/Views/Shared/Error.cshtml @@ -0,0 +1,6 @@ +@{ + ViewData["Title"] = "Error"; +} + +

Error.

+

An error occurred while processing your request.

diff --git a/samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml b/samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml new file mode 100755 index 000000000000..2e8aa72d3107 --- /dev/null +++ b/samples/misc/ES2015Transpilation/Views/Shared/_Layout.cshtml @@ -0,0 +1,11 @@ + + + + + ES2015 Example + + + @RenderBody() + @RenderSection("scripts", required: false) + + diff --git a/samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml b/samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml new file mode 100755 index 000000000000..7b6d0ab29090 --- /dev/null +++ b/samples/misc/ES2015Transpilation/Views/_ViewImports.cshtml @@ -0,0 +1,2 @@ +@using ES2015Example +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" diff --git a/samples/misc/ES2015Transpilation/Views/_ViewStart.cshtml b/samples/misc/ES2015Transpilation/Views/_ViewStart.cshtml new file mode 100755 index 000000000000..66b5da255ace --- /dev/null +++ b/samples/misc/ES2015Transpilation/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/samples/misc/ES2015Transpilation/appsettings.json b/samples/misc/ES2015Transpilation/appsettings.json new file mode 100755 index 000000000000..0967ef424bce --- /dev/null +++ b/samples/misc/ES2015Transpilation/appsettings.json @@ -0,0 +1 @@ +{} diff --git a/samples/misc/ES2015Transpilation/jsconfig.json b/samples/misc/ES2015Transpilation/jsconfig.json new file mode 100644 index 000000000000..875bb90cd697 --- /dev/null +++ b/samples/misc/ES2015Transpilation/jsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs" + } +} diff --git a/samples/misc/ES2015Transpilation/package.json b/samples/misc/ES2015Transpilation/package.json new file mode 100644 index 000000000000..c3436cf53a91 --- /dev/null +++ b/samples/misc/ES2015Transpilation/package.json @@ -0,0 +1,9 @@ +{ + "name": "ES2015Example", + "version": "0.0.0", + "dependencies": { + "babel-core": "^5.8.29", + "body-parser": "^1.14.1", + "express": "^4.13.3" + } +} diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json new file mode 100755 index 000000000000..f6542c94c0a9 --- /dev/null +++ b/samples/misc/ES2015Transpilation/project.json @@ -0,0 +1,45 @@ +{ + "webroot": "wwwroot", + "version": "1.0.0-*", + "tooling": { + "defaultNamespace": "ES2015Example" + }, + "dependencies": { + "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", + "Microsoft.AspNet.Mvc": "6.0.0-beta8", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", + "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", + "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", + "Microsoft.Framework.Logging": "1.0.0-beta8", + "Microsoft.Framework.Logging.Console": "1.0.0-beta8", + "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha1" + }, + "commands": { + "web": "Microsoft.AspNet.Server.Kestrel" + }, + "frameworks": { + "dnx451": {}, + "dnxcore50": {} + }, + "exclude": [ + "wwwroot", + "node_modules", + "bower_components" + ], + "publishExclude": [ + "node_modules", + "bower_components", + "**.xproj", + "**.user", + "**.vspscc" + ], + "scripts": { + "prepublish": [ + "npm install" + ] + } +} \ No newline at end of file diff --git a/samples/misc/ES2015Transpilation/transpilation.js b/samples/misc/ES2015Transpilation/transpilation.js new file mode 100644 index 000000000000..785e59bd2ba3 --- /dev/null +++ b/samples/misc/ES2015Transpilation/transpilation.js @@ -0,0 +1,6 @@ +var babelCore = require('babel-core'); + +module.exports = function(cb, fileContents) { + var result = babelCore.transform(fileContents, {}); + cb(null, result.code); +} diff --git a/samples/misc/ES2015Transpilation/wwwroot/favicon.ico b/samples/misc/ES2015Transpilation/wwwroot/favicon.ico new file mode 100755 index 0000000000000000000000000000000000000000..a3a799985c43bc7309d701b2cad129023377dc71 GIT binary patch literal 32038 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 new TypeError("Only one wildcard in a path is permitted");if(1==s.length){if(t==o){a=o;break}}else{var i=o.split("/").length;i>=r&&t.substr(0,s[0].length)==s[0]&&t.substr(t.length-s[1].length)==s[1]&&(r=i,a=o,n=t.substr(s[0].length,t.length-s[1].length-s[0].length))}}var l=e[a]||t;return n&&(l=l.replace("*",n)),l}function i(){}function l(){o.call(this),k.call(this)}function u(){}function d(e,t){l.prototype[e]=t(l.prototype[e])}function c(e){k=e(k||function(){})}function f(e){for(var t=[],n=0,a=e.length;a>n;n++)-1==x.call(t,e[n])&&t.push(e[n]);return t}function m(e,t,n){for(var a in t)n&&a in e||(e[a]=t[a])}function p(e,t){for(var n=e.split(".");n.length;)t=t[n.shift()];return t}function h(){if(O[this.baseURL])return O[this.baseURL];"/"!=this.baseURL[this.baseURL.length-1]&&(this.baseURL+="/");var e=new _(this.baseURL,w);return this.baseURL=e.href,O[this.baseURL]=e}var v="undefined"==typeof window&&"undefined"!=typeof self&&"undefined"!=typeof importScripts,g="undefined"!=typeof window&&"undefined"!=typeof document,y="undefined"!=typeof process&&!!process.platform.match(/^win/);e.console||(e.console={assert:function(){}});var b,x=Array.prototype.indexOf||function(e){for(var t=0,n=this.length;n>t;t++)if(this[t]===e)return t;return-1};!function(){try{Object.defineProperty({},"a",{})&&(b=Object.defineProperty)}catch(e){b=function(e,t,n){try{e[t]=n.value||n.get.call(e)}catch(a){}}}}();var w;if("undefined"!=typeof document&&document.getElementsByTagName){if(w=document.baseURI,!w){var S=document.getElementsByTagName("base");w=S[0]&&S[0].href||window.location.href}w=w.split("#")[0].split("?")[0],w=w.substr(0,w.lastIndexOf("/")+1)}else if("undefined"!=typeof process&&process.cwd)w="file://"+(y?"/":"")+process.cwd()+"/",y&&(w=w.replace(/\\/g,"/"));else{if("undefined"==typeof location)throw new TypeError("No environment baseURI");w=e.location.href}var _=e.URLPolyfill||e.URL;!function(){function o(e){return{status:"loading",name:e,linkSets:[],dependencies:[],metadata:{}}}function s(e,t,n){return new Promise(c({step:n.address?"fetch":"locate",loader:e,moduleName:t,moduleMetadata:n&&n.metadata||{},moduleSource:n.source,moduleAddress:n.address}))}function i(e,t,n,a){return new Promise(function(r,o){r(e.loaderObj.normalize(t,n,a))}).then(function(t){var n;if(e.modules[t])return n=o(t),n.status="linked",n.module=e.modules[t],n;for(var a=0,r=e.loads.length;r>a;a++)if(n=e.loads[a],n.name==t)return n;return n=o(t),e.loads.push(n),l(e,n),n})}function l(e,t){u(e,t,Promise.resolve().then(function(){return e.loaderObj.locate({name:t.name,metadata:t.metadata})}))}function u(e,t,n){d(e,t,n.then(function(n){return"loading"==t.status?(t.address=n,e.loaderObj.fetch({name:t.name,metadata:t.metadata,address:n})):void 0}))}function d(t,a,r){r.then(function(r){return"loading"==a.status?Promise.resolve(t.loaderObj.translate({name:a.name,metadata:a.metadata,address:a.address,source:r})).then(function(e){return a.source=e,t.loaderObj.instantiate({name:a.name,metadata:a.metadata,address:a.address,source:e})}).then(function(r){if(void 0===r)return a.address=a.address||"",a.isDeclarative=!0,j.call(t.loaderObj,a).then(function(t){var r=e.System,o=r.register;r.register=function(e,t,n){"string"!=typeof e&&(n=t,t=e),a.declare=n,a.depsList=t},n(t,a.address,{}),r.register=o});if("object"!=typeof r)throw TypeError("Invalid instantiate return value");a.depsList=r.deps||[],a.execute=r.execute,a.isDeclarative=!1}).then(function(){a.dependencies=[];for(var e=a.depsList,n=[],r=0,o=e.length;o>r;r++)(function(e,r){n.push(i(t,e,a.name,a.address).then(function(t){if(a.dependencies[r]={key:e,value:t.name},"linked"!=t.status)for(var n=a.linkSets.concat([]),o=0,s=n.length;s>o;o++)m(n[o],t)}))})(e[r],r);return Promise.all(n)}).then(function(){a.status="loaded";for(var e=a.linkSets.concat([]),t=0,n=e.length;n>t;t++)h(e[t],a)}):void 0})["catch"](function(e){a.status="failed",a.exception=e;for(var t=a.linkSets.concat([]),n=0,r=t.length;r>n;n++)v(t[n],a,e)})}function c(e){return function(t,n){var a=e.loader,r=e.moduleName,s=e.step;if(a.modules[r])throw new TypeError('"'+r+'" already exists in the module table');for(var i,c=0,m=a.loads.length;m>c;c++)if(a.loads[c].name==r)return i=a.loads[c],"translate"!=s||i.source||(i.address=e.moduleAddress,d(a,i,Promise.resolve(e.moduleSource))),i.linkSets[0].done.then(function(){t(i)});var p=o(r);p.metadata=e.moduleMetadata;var h=f(a,p);a.loads.push(p),t(h.done),"locate"==s?l(a,p):"fetch"==s?u(a,p,Promise.resolve(e.moduleAddress)):(p.address=e.moduleAddress,d(a,p,Promise.resolve(e.moduleSource)))}}function f(e,t){var n={loader:e,loads:[],startingLoad:t,loadingCount:0};return n.done=new Promise(function(e,t){n.resolve=e,n.reject=t}),m(n,t),n}function m(e,t){for(var n=0,a=e.loads.length;a>n;n++)if(e.loads[n]==t)return;e.loads.push(t),t.linkSets.push(e),"loaded"!=t.status&&e.loadingCount++;for(var r=e.loader,n=0,a=t.dependencies.length;a>n;n++){var o=t.dependencies[n].value;if(!r.modules[o])for(var s=0,i=r.loads.length;i>s;s++)if(r.loads[s].name==o){m(e,r.loads[s]);break}}}function p(e){var t=!1;try{S(e,function(n,a){v(e,n,a),t=!0})}catch(n){v(e,null,n),t=!0}return t}function h(e,t){if(e.loadingCount--,!(e.loadingCount>0)){var n=e.startingLoad;if(e.loader.loaderObj.execute===!1){for(var a=[].concat(e.loads),r=0,o=a.length;o>r;r++){var t=a[r];t.module=t.isDeclarative?{name:t.name,module:M({}),evaluated:!0}:{module:M({})},t.status="linked",g(e.loader,t)}return e.resolve(n)}var s=p(e);s||e.resolve(n)}}function v(e,n,a){var r=e.loader;n?(n&&e.loads[0].name!=n.name&&(a=t(a,"Error loading "+n.name+" from "+e.loads[0].name)),n&&(a=t(a,"Error loading "+n.name))):a=t(a,"Error linking "+e.loads[0].name);for(var o=e.loads.concat([]),s=0,i=o.length;i>s;s++){var n=o[s];r.loaderObj.failed=r.loaderObj.failed||[],-1==x.call(r.loaderObj.failed,n)&&r.loaderObj.failed.push(n);var l=x.call(n.linkSets,e);if(n.linkSets.splice(l,1),0==n.linkSets.length){var u=x.call(e.loader.loads,n);-1!=u&&e.loader.loads.splice(u,1)}}e.reject(a)}function g(e,t){if(e.loaderObj.trace){e.loaderObj.loads||(e.loaderObj.loads={});var n={};t.dependencies.forEach(function(e){n[e.key]=e.value}),e.loaderObj.loads[t.name]={name:t.name,deps:t.dependencies.map(function(e){return e.key}),depMap:n,address:t.address,metadata:t.metadata,source:t.source,kind:t.isDeclarative?"declarative":"dynamic"}}t.name&&(e.modules[t.name]=t.module);var a=x.call(e.loads,t);-1!=a&&e.loads.splice(a,1);for(var r=0,o=t.linkSets.length;o>r;r++)a=x.call(t.linkSets[r].loads,t),-1!=a&&t.linkSets[r].loads.splice(a,1);t.linkSets.splice(0,t.linkSets.length)}function y(e,t,n){try{var r=t.execute()}catch(o){return void n(t,o)}return r&&r instanceof a?r:void n(t,new TypeError("Execution must define a Module instance"))}function w(e,t,n){var a=e._loader.importPromises;return a[t]=n.then(function(e){return a[t]=void 0,e},function(e){throw a[t]=void 0,e})}function S(e,t){var n=e.loader;if(e.loads.length)for(var a=e.loads.concat([]),r=0;rr;r++){var s=e.normalizedDeps[r],i=t.defined[s];if(i&&!i.evaluated){var l=e.groupIndex+(i.declarative!=e.declarative);if(void 0===i.groupIndex||i.groupIndex=0;l--){for(var u=r[l],d=0;da;a++){var o=s.importers[a];if(!o.locked){var l=x.call(o.dependencies,s);o.setters[l](i)}}return s.locked=!1,t});if(s.setters=l.setters,s.execute=l.execute,!s.setters||!s.execute)throw new TypeError("Invalid System.register form for "+t.name);for(var u=0,d=t.normalizedDeps.length;d>u;u++){var c,f=t.normalizedDeps[u],m=n.defined[f],p=a[f];p?c=p.exports:m&&!m.declarative?c=m.esModule:m?(o(m,n),p=m.module,c=p.exports):c=n.get(f),p&&p.importers?(p.importers.push(s),s.dependencies.push(p)):s.dependencies.push(null),s.setters[u]&&s.setters[u](c)}}}function s(e,t){var n,a=t.defined[e];if(a)a.declarative?u(e,[],t):a.evaluated||i(a,t),n=a.module.exports;else if(n=t.get(e),!n)throw new Error("Unable to load dependency "+e+".");return(!a||a.declarative)&&n&&n.__useDefault?n["default"]:n}function i(t,n){if(!t.module){var a={},r=t.module={exports:a,id:t.name};if(!t.executingRequire)for(var o=0,l=t.normalizedDeps.length;l>o;o++){var u=t.normalizedDeps[o],d=n.defined[u];d&&i(d,n)}t.evaluated=!0;var c=t.execute.call(e,function(e){for(var a=0,r=t.deps.length;r>a;a++)if(t.deps[a]==e)return s(t.normalizedDeps[a],n);throw new TypeError("Module "+e+" not declared as a dependency.")},a,r);if(c&&(r.exports=c),a=r.exports,a&&a.__esModule)t.esModule=a;else{if(t.esModule={},"object"==typeof a||"function"==typeof a)if(Object.getOwnPropertyDescriptor){var f;for(var m in a)(f=Object.getOwnPropertyDescriptor(a,m))&&Object.defineProperty(t.esModule,m,f)}else{var p=a&&a.hasOwnProperty;for(var m in a)(!p||a.hasOwnProperty(m))&&(t.esModule[m]=a[m])}t.esModule["default"]=a,b(t.esModule,"__useDefault",{value:!0})}}}function u(t,n,a){var r=a.defined[t];if(r&&!r.evaluated&&r.declarative){n.push(t);for(var o=0,s=r.normalizedDeps.length;s>o;o++){var i=r.normalizedDeps[o];-1==x.call(n,i)&&(a.defined[i]?u(i,n,a):a.get(i))}r.evaluated||(r.evaluated=!0,r.module.execute.call(e))}}var m,p;l.prototype.register=function(e,n,a){return"string"!=typeof e&&(a=n,n=e,e=null),"boolean"==typeof a?this.registerDynamic.apply(this,arguments):void t(this,e,{declarative:!0,deps:n,declare:a})},l.prototype.registerDynamic=function(e,n,a,r){"string"!=typeof e&&(r=a,a=n,n=e,e=null),t(this,e,{declarative:!1,deps:n,execute:r,executingRequire:a})},c(function(e){return function(){e.call(this),this.defined={},this._loader.moduleRecords={}}}),d("onScriptLoad",function(e){return function(t){e.call(this,t),m&&(t.metadata.entry=m),p&&(t.metadata.format=t.metadata.format||"defined",t.metadata.registered=!0,p=!1,m=null)}}),d("delete",function(e){return function(t){return delete this._loader.moduleRecords[t],delete this.defined[t],e.call(this,t)}});var h=/^\s*(\/\*.*\*\/\s*|\/\/[^\n]*\s*)*System\.register(Dynamic)?\s*\(/;d("fetch",function(e){return function(t){return this.defined[t.name]?(t.metadata.format="defined",""):(m=null,p=!1,"register"==t.metadata.format&&(t.metadata.scriptLoad=!0),t.metadata.deps=t.metadata.deps||[],e.call(this,t))}}),d("translate",function(e){return function(t){return Promise.resolve(e.call(this,t)).then(function(e){return"string"==typeof t.metadata.deps&&(t.metadata.deps=t.metadata.deps.split(",")),t.metadata.deps=t.metadata.deps||[],("register"==t.metadata.format||!t.metadata.format&&t.source.match(h))&&(t.metadata.format="register"),e})}}),d("instantiate",function(e){return function(e){var t,n=this;if(n.defined[e.name])t=n.defined[e.name],t.deps=t.deps.concat(e.metadata.deps);else if(e.metadata.entry)t=e.metadata.entry;else if(e.metadata.execute)t={declarative:!1,deps:e.metadata.deps||[],execute:e.metadata.execute,executingRequire:e.metadata.executingRequire};else if(!("register"!=e.metadata.format&&"esm"!=e.metadata.format&&"es6"!=e.metadata.format||(m=null,p=!1,M.call(n,e),m?t=m:e.metadata.bundle=!0,!t&&n.defined[e.name]&&(t=n.defined[e.name]),p||e.metadata.registered)))throw new TypeError(e.name+" detected as System.register but didn't execute.");t||(t={declarative:!1,deps:e.metadata.deps,execute:function(){return n.newModule({})}}),n.defined[e.name]=t,t.deps=f(t.deps),t.name=e.name;for(var r=[],o=0,s=t.deps.length;s>o;o++)r.push(Promise.resolve(n.normalize(t.deps[o],e.name)));return Promise.all(r).then(function(r){return t.normalizedDeps=r,{deps:t.deps,execute:function(){return a(e.name,n),u(e.name,[],n),n.defined[e.name]=void 0,n.newModule(t.declarative?t.module.exports:t.esModule)}}})}})}(),function(){var t=/(^\s*|[}\);\n]\s*)(import\s+(['"]|(\*\s+as\s+)?[^"'\(\)\n;]+\s+from\s+['"]|\{)|export\s+\*\s+from\s+["']|export\s+(\{|default|function|class|var|const|let|async\s+function))/,n=/\$traceurRuntime\s*\./,a=/babelHelpers\s*\./;d("translate",function(r){return function(o){var s=this;return r.call(s,o).then(function(r){if("esm"==o.metadata.format||"es6"==o.metadata.format||!o.metadata.format&&r.match(t))return o.metadata.format="esm",s._loadedTranspiler=s._loadedTranspiler||!1,s.pluginLoader&&(s.pluginLoader._loadedTranspiler=s._loadedTranspiler||!1),R.call(s,o).then(function(e){return o.metadata.sourceMap=void 0,e});if(s._loadedTranspiler===!1&&o.name==s.normalizeSync(s.transpiler)&&(r.length>100&&(o.metadata.format=o.metadata.format||"global","traceur"===s.transpiler&&(o.metadata.exports="traceur"),"typescript"===s.transpiler&&(o.metadata.exports="ts")),s._loadedTranspiler=!0),s._loadedTranspilerRuntime===!1&&(o.name==s.normalizeSync("traceur-runtime")||o.name==s.normalizeSync("babel/external-helpers*"))&&(r.length>100&&(o.metadata.format=o.metadata.format||"global"),s._loadedTranspilerRuntime=!0),"register"==o.metadata.format&&s._loadedTranspilerRuntime!==!0){if(!e.$traceurRuntime&&o.source.match(n))return s._loadedTranspilerRuntime=s._loadedTranspilerRuntime||!1,s["import"]("traceur-runtime").then(function(){return r});if(!e.babelHelpers&&o.source.match(a))return s._loadedTranspilerRuntime=s._loadedTranspilerRuntime||!1,s["import"]("babel/external-helpers").then(function(){return r})}return r})}})}();var T="undefined"!=typeof self?"self":"global";d("onScriptLoad",function(t){return function(n){if("global"==n.metadata.format){n.metadata.registered=!0;var a=p(n.metadata.exports,e);n.metadata.execute=function(){return a}}return t.call(this,n)}}),d("fetch",function(e){return function(t){return t.metadata.exports&&(t.metadata.format="global"),"global"!=t.metadata.format||!t.metadata.exports||t.metadata.globals||t.metadata.deps&&0!=t.metadata.deps.length||(t.metadata.scriptLoad=!0),e.call(this,t)}}),d("instantiate",function(t){return function(n){var a=this;if(n.metadata.format||(n.metadata.format="global"),n.metadata.globals)for(var r in n.metadata.globals)n.metadata.deps.push(n.metadata.globals[r]);return"global"!=n.metadata.format||n.metadata.registered||(n.metadata.execute=function(t,r,o){var s;if(n.metadata.globals){s={};for(var i in n.metadata.globals)s[i]=t(n.metadata.globals[i])}var l=n.metadata.exports,u=a.get("@@global-helpers").prepareGlobal(o.id,l,s);l&&(n.source+="\n"+T+'["'+l+'"] = '+l+";");var d=e.define,c=e.require;return e.define=void 0,e.module=void 0,e.exports=void 0,M.call(a,n),e.require=c,e.define=d,u()}),t.call(this,n)}}),c(function(t){return function(){function n(t){if(Object.keys)Object.keys(e).forEach(t);else for(var n in e)s.call(e,n)&&t(n)}function a(t){n(function(n){if(-1==x.call(i,n)){try{var a=e[n]}catch(r){i.push(n)}t(n,a)}})}var r=this;t.call(r);var o,s=Object.prototype.hasOwnProperty,i=["_g","sessionStorage","localStorage","clipboardData","frames","external"];r.set("@@global-helpers",r.newModule({prepareGlobal:function(t,n,r){var s;if(r){s={};for(var i in r)s[i]=r[i],e[i]=r[i]}return n||(o={},a(function(e,t){o[e]=t})),function(){var t;if(n)t=p(n,e);else{var r,i,l={};a(function(e,t){o[e]!==t&&"undefined"!=typeof t&&(l[e]=t,"undefined"!=typeof r?i||r===t||(i=!0):r=t)}),t=i?l:r}if(s)for(var u in s)e[u]=s[u];return t}}}))}}),function(){function t(e){a.lastIndex=0;var t=[];e.length/e.split("\n").length<200&&(e=e.replace(r,""));for(var n;n=a.exec(e);)t.push(n[1].substr(1,n[1].length-2));return t}var n=/(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.]|module\.)exports\s*(\[['"]|\.)|(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF.])module\.exports\s*[=,]/,a=/(?:^\uFEFF?|[^$_a-zA-Z\xA0-\uFFFF."'])require\s*\(\s*("[^"\\]*(?:\\.[^"\\]*)*"|'[^'\\]*(?:\\.[^'\\]*)*')\s*\)/g,r=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/gm;if("undefined"!=typeof window&&"undefined"!=typeof document&&window.location)var o=location.protocol+"//"+location.hostname+(location.port?":"+location.port:"");c(function(e){return function(){e.call(this),"undefined"!=typeof require&&require.resolve&&"undefined"!=typeof process&&(this._nodeRequire=require)}}),d("instantiate",function(r){return function(s){var i=this;if(s.metadata.format||(n.lastIndex=0,a.lastIndex=0,(a.exec(s.source)||n.exec(s.source))&&(s.metadata.format="cjs")),"cjs"==s.metadata.format){var l=s.metadata.deps||[];s.metadata.deps=l.concat(t(s.source)),s.metadata.executingRequire=!0,s.metadata.execute=function(t,n,a){for(var r=0;r=i)continue;r=s,o=i}r&&(t=this.map[r]+t.substr(r.length))}return e.call(this,t,n,a)}}),d("normalize",function(e){return function(t,n){var a=e.call(this,t,n);return this.has(a)?a:a.match(L)?(this.defaultJSExtensions&&".js"!=a.substr(a.length-3,3)&&(a+=".js"),a):(a=s(this.paths,a)||a,this.defaultJSExtensions&&".js"!=a.substr(a.length-3,3)&&(a+=".js"),"."==a[0]||"/"==a[0]?new _(a,P).href:new _(a,h.call(this)).href)}}),function(){function e(e){for(var t in this.packages)if(e.substr(0,t.length)===t&&(e.length===t.length||"/"===e[t.length]))return t}function t(e,t){var n=e.packages[t];return n.env?e["import"](n.map["@env"]||"@system-env",t).then(function(a){ +var r={};for(var o in n)"map"!==o&"env"!==o&&(r[o]=n[o]);r.map={};for(var o in n.map)r.map[o]=n.map[o];for(var s in n.env)if(a[s]){var i=n.env[s];i.main&&(r.main=i.main);for(var l in i.map)r.map[l]=i.map[l]}return e.packages[t]=r,r}):Promise.resolve(n)}function n(e,t){var n,a=0;for(var r in e)if(t.substr(0,r.length)==r&&(t.length==r.length||"/"==t[r.length])){var o=r.split("/").length;if(a>=o)continue;n=r,a=o}return n?e[n]+t.substr(n.length):void 0}c(function(e){return function(){e.call(this),this.packages={}}}),l.prototype.normalizeSync=l.prototype.normalize,d("normalize",function(a){return function(r,o){if(o)var s=e.call(this,o)||this.defaultJSExtensions&&".js"==o.substr(o.length-3,3)&&e.call(this,o.substr(0,o.length-3));if(s&&"."!==r[0]){var i=this.packages[s].map;i&&(r=n(i,r)||r,"."===r[0]&&(o=s+"/"))}var l=this.defaultJSExtensions&&".js"!=r.substr(r.length-3,3),u=a.call(this,r,o);".js"!=u.substr(u.length-3,3)&&(l=!1),l&&(u=u.substr(0,u.length-3));var d=e.call(this,u);return d?t(this,d).then(function(e){if(d===u&&e.main&&(u+="/"+("./"==e.main.substr(0,2)?e.main.substr(2):e.main)),"/"==u.substr(d.length))return u;var t="";e.meta&&e.meta[u.substr(d.length+1)]||("defaultExtension"in e?e.defaultExtension!==!1&&-1==u.split("/").pop().indexOf(".")&&(t="."+e.defaultExtension):l&&(t=".js"));var a="."+u.substr(d.length),r=n(e.map,a)||t&&n(e.map,a+t);return r?u="./"==r.substr(0,2)?d+r.substr(1):r:u+=t,u}):(l&&(u+=".js"),u)}}),d("locate",function(t){return function(n){var a=this;return Promise.resolve(t.call(this,n)).then(function(t){var r=e.call(a,n.name);if(r){var o=a.packages[r];if(o.format&&(n.metadata.format=n.metadata.format||o.format),o.loader&&(n.metadata.loader=n.metadata.loader||o.loader),o.meta){var s,i={},l=0;for(var u in o.meta)if(s=u.indexOf("*"),-1!==s&&u.substr(0,s)===n.name.substr(0,s)&&u.substr(s+1)===n.name.substr(n.name.length-u.length+s+1)){var d=u.split("/").length;d>l&&(bestDetph=d),m(i,o.meta[u],l!=d)}var c=o.meta[n.name.substr(r.length+1)];c&&m(i,c),i.alias&&"./"==i.alias.substr(0,2)&&(i.alias=r+i.alias.substr(1)),i.loader&&"./"==i.loader.substr(0,2)&&(i.loader=r+i.loader.substr(1)),m(n.metadata,i)}}return t})}})}(),function(){function e(e,t,n,a){var r,o=this;n&&-1!=(r=n.indexOf("!"))&&(n=n.substr(0,r));var s=t.lastIndexOf("!");if(-1!=s){var i=t.substr(0,s),l=t.substr(s+1)||i.substr(i.lastIndexOf(".")+1),u=o.defaultJSExtensions&&".js"!=i.substr(i.length-3,3);return a?(i=o.normalizeSync(i,n),l=o.normalizeSync(l,n),u&&".js"==i.substr(i.length-3,3)&&(i=i.substr(0,i.length-3)),i+"!"+l):Promise.all([o.normalize(i,n),o.normalize(l,n)]).then(function(e){return i=e[0],u&&".js"==i.substr(i.length-3,3)&&(i=i.substr(0,i.length-3)),i+"!"+e[1]})}return e.call(o,t,n)}d("normalize",function(t){return function(n,a){return e.call(this,t,n,a,!1)}}),d("normalizeSync",function(t){return function(n,a){return e.call(this,t,n,a,!0)}}),d("locate",function(e){return function(t){var n=this,a=t.name,r=a.lastIndexOf("!");return-1!=r&&(t.metadata.loader=a.substr(r+1),t.name=a.substr(0,r)),e.call(n,t).then(function(e){var r=t.metadata.loader;if(!r)return e;if(n.defined&&n.defined[a])return e;var o=n.pluginLoader||n;return o["import"](r).then(function(r){return t.metadata.loaderModule=r,t.metadata.loaderArgument=a,t.address=e,r.locate?r.locate.call(n,t):e})})}}),d("fetch",function(e){return function(t){var n=this;return t.metadata.loaderModule&&t.metadata.loaderModule.fetch?(t.metadata.scriptLoad=!1,t.metadata.loaderModule.fetch.call(n,t,function(t){return e.call(n,t)})):e.call(n,t)}}),d("translate",function(e){return function(t){var n=this;return t.metadata.loaderModule&&t.metadata.loaderModule.translate?Promise.resolve(t.metadata.loaderModule.translate.call(n,t)).then(function(a){return"string"==typeof a&&(t.source=a),e.call(n,t)}):e.call(n,t)}}),d("instantiate",function(e){return function(t){var n=this;return t.metadata.loaderModule&&t.metadata.loaderModule.instantiate?Promise.resolve(t.metadata.loaderModule.instantiate.call(n,t)).then(function(a){return t.metadata.format="defined",t.metadata.execute=function(){return a},e.call(n,t)}):e.call(n,t)}})}(),function(){d("fetch",function(e){return function(t){var n=t.metadata.alias;return n?(t.metadata.format="defined",this.defined[t.name]={declarative:!0,deps:[n],declare:function(e){return{setters:[function(t){for(var n in t)e(n,t[n])}],execute:function(){}}}},""):e.call(this,t)}})}(),function(){function e(e,t,n){for(var a,r=t.split(".");r.length>1;)a=r.shift(),e=e[a]=e[a]||{};a=r.shift(),a in e||(e[a]=n)}c(function(e){return function(){this.meta={},e.call(this)}}),d("locate",function(e){return function(t){var n,a=this.meta,r=t.name,o=0;for(var s in a)if(n=x.call(s,"*"),-1!==n&&s.substr(0,n)===r.substr(0,n)&&s.substr(n+1)===r.substr(r.length-s.length+n+1)){var i=s.split("/").length;i>o&&(bestDetph=i),m(t.metadata,a[s],o!=i)}return a[r]&&m(t.metadata,a[r]),e.call(this,t)}});var t=/^(\s*\/\*.*\*\/|\s*\/\/[^\n]*|\s*"[^"]+"\s*;?|\s*'[^']+'\s*;?)+/,n=/\/\*.*\*\/|\/\/[^\n]*|"[^"]+"\s*;?|'[^']+'\s*;?/g;d("translate",function(a){return function(r){var o=r.source.match(t);if(o)for(var s=o[0].match(n),i=0;i')}else if("undefined"!=typeof importScripts){var o="";try{throw new Error("_")}catch(n){n.stack.replace(/(?:at|@).*(http.+):[\d]+:[\d]+/,function(e,t){o=t.replace(/\/[^\/]*$/,"/")})}importScripts(o+"system-polyfills.js"),e()}else e()}(); diff --git a/samples/misc/ES2015Transpilation/wwwroot/web.config b/samples/misc/ES2015Transpilation/wwwroot/web.config new file mode 100644 index 000000000000..db6e6f4582dc --- /dev/null +++ b/samples/misc/ES2015Transpilation/wwwroot/web.config @@ -0,0 +1,9 @@ + + + + + + + + + From 301657a2071d1d7e693f63acb1d1b2ea99242f76 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 11:32:32 -0800 Subject: [PATCH 0006/1585] Enable inline source maps --- .../ES2015Transpilation/Controllers/ScriptController.cs | 2 +- samples/misc/ES2015Transpilation/transpilation.js | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs index 62f0878a5efc..cfb1cbfc6c92 100644 --- a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs +++ b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs @@ -12,7 +12,7 @@ public async Task Transpile(string filename) { // TODO: Don't hard-code wwwroot; use proper path conversions var fileContents = System.IO.File.ReadAllText("wwwroot/" + filename); - var transpiledResult = await nodeInstance.Invoke("transpilation.js", fileContents); + var transpiledResult = await nodeInstance.Invoke("transpilation.js", fileContents, Request.Path.Value); return Content(transpiledResult, "application/javascript"); } } diff --git a/samples/misc/ES2015Transpilation/transpilation.js b/samples/misc/ES2015Transpilation/transpilation.js index 785e59bd2ba3..ac239e580883 100644 --- a/samples/misc/ES2015Transpilation/transpilation.js +++ b/samples/misc/ES2015Transpilation/transpilation.js @@ -1,6 +1,9 @@ var babelCore = require('babel-core'); -module.exports = function(cb, fileContents) { - var result = babelCore.transform(fileContents, {}); +module.exports = function(cb, fileContents, url) { + var result = babelCore.transform(fileContents, { + sourceMaps: 'inline', + sourceFileName: '/sourcemapped/' + url + }); cb(null, result.code); } From de991b98586492d0ab7afb7c45fdae1c7b4f4782 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 13:35:14 -0800 Subject: [PATCH 0007/1585] Switch to using DI to acquire Node instances. Bump versions to alpha2. --- .../AngularPrerenderTagHelper.cs | 8 ++-- .../project.json | 4 +- .../ReactRenderer.cs | 5 +-- .../project.json | 4 +- .../Configuration.cs | 24 ++++++++++++ .../{HttpNodeHost.cs => HttpNodeInstance.cs} | 6 +-- ...st.cs => InputOutputStreamNodeInstance.cs} | 6 +-- .../HostingModels/NodeHost.cs | 10 ----- ...eRunner.cs => OutOfProcessNodeInstance.cs} | 28 ++++++++++---- .../INodeInstance.cs | 10 +++++ Microsoft.AspNet.NodeServices/NodeInstance.cs | 38 ------------------- .../EmbeddedResourceReader.cs | 0 .../{ => Util}/StringAsTempFile.cs | 0 Microsoft.AspNet.NodeServices/project.json | 5 ++- samples/angular/MusicStore/Startup.cs | 14 ++----- samples/angular/MusicStore/project.json | 2 +- .../Controllers/HomeController.cs | 2 +- .../Controllers/ScriptController.cs | 8 +++- samples/misc/ES2015Transpilation/Startup.cs | 4 ++ samples/misc/ES2015Transpilation/project.json | 2 +- .../ReactGrid/Controllers/HomeController.cs | 9 ++++- samples/react/ReactGrid/Startup.cs | 4 ++ samples/react/ReactGrid/project.json | 2 +- 23 files changed, 103 insertions(+), 92 deletions(-) create mode 100644 Microsoft.AspNet.NodeServices/Configuration.cs rename Microsoft.AspNet.NodeServices/HostingModels/{HttpNodeHost.cs => HttpNodeInstance.cs} (89%) rename Microsoft.AspNet.NodeServices/HostingModels/{InputOutputStreamNodeHost.cs => InputOutputStreamNodeInstance.cs} (93%) delete mode 100644 Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs rename Microsoft.AspNet.NodeServices/HostingModels/{OutOfProcessNodeRunner.cs => OutOfProcessNodeInstance.cs} (85%) create mode 100644 Microsoft.AspNet.NodeServices/INodeInstance.cs delete mode 100644 Microsoft.AspNet.NodeServices/NodeInstance.cs rename Microsoft.AspNet.NodeServices/{HostingModels => Util}/EmbeddedResourceReader.cs (100%) rename Microsoft.AspNet.NodeServices/{ => Util}/StringAsTempFile.cs (100%) diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs index 9a5292b80a98..31e4602c40ae 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs @@ -20,8 +20,6 @@ static AngularRunAtServerTagHelper() { const string PrerenderModuleAttributeName = "aspnet-ng2-prerender-module"; const string PrerenderExportAttributeName = "aspnet-ng2-prerender-export"; - private static NodeInstance nodeInstance = new NodeInstance(); - [HtmlAttributeName(PrerenderModuleAttributeName)] public string ModuleName { get; set; } @@ -29,15 +27,17 @@ static AngularRunAtServerTagHelper() { public string ExportName { get; set; } private IHttpContextAccessor contextAccessor; + private INodeServices nodeServices; - public AngularRunAtServerTagHelper(IHttpContextAccessor contextAccessor) + public AngularRunAtServerTagHelper(INodeServices nodeServices, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; + this.nodeServices = nodeServices; } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var result = await nodeInstance.InvokeExport(nodeScript.FileName, "renderComponent", new { + var result = await this.nodeServices.InvokeExport(nodeScript.FileName, "renderComponent", new { componentModule = this.ModuleName, componentExport = this.ExportName, tagName = output.TagName, diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json index 133451b8d406..fec49f8b24ca 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha1", + "version": "1.0.0-alpha2", "description": "Microsoft.AspNet.NodeServices.Angular Class Library", "authors": [ "Microsoft" @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha1", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha2", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" }, "resource": [ diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs index 653e52ae31ff..4094a2a43742 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs +++ b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs @@ -5,7 +5,6 @@ namespace Microsoft.AspNet.NodeServices.React public static class ReactRenderer { private static StringAsTempFile nodeScript; - private static NodeInstance nodeInstance = new NodeInstance(); static ReactRenderer() { // Consider populating this lazily @@ -13,8 +12,8 @@ static ReactRenderer() { nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit } - public static async Task RenderToString(string moduleName, string exportName, string baseUrl) { - return await nodeInstance.InvokeExport(nodeScript.FileName, "renderToString", new { + public static async Task RenderToString(INodeServices nodeServices, string moduleName, string exportName, string baseUrl) { + return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { moduleName, exportName, baseUrl diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json index 3500b7301170..a0998400a205 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha1", + "version": "1.0.0-alpha2", "description": "Microsoft.AspNet.NodeServices.React Class Library", "authors": [ "Microsoft" @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha1" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha2" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.NodeServices/Configuration.cs b/Microsoft.AspNet.NodeServices/Configuration.cs new file mode 100644 index 000000000000..c0c336e7ea37 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/Configuration.cs @@ -0,0 +1,24 @@ +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.NodeServices { + public static class Configuration { + public static void AddNodeServices(this IServiceCollection serviceCollection, NodeHostingModel hostingModel = NodeHostingModel.Http) { + serviceCollection.AddSingleton(typeof(INodeServices), (serviceProvider) => { + return CreateNodeServices(hostingModel); + }); + } + + private static INodeServices CreateNodeServices(NodeHostingModel hostingModel) + { + switch (hostingModel) + { + case NodeHostingModel.Http: + return new HttpNodeInstance(); + case NodeHostingModel.InputOutputStream: + return new InputOutputStreamNodeInstance(); + default: + throw new System.ArgumentException("Unknown hosting model: " + hostingModel.ToString()); + } + } + } +} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs similarity index 89% rename from Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs rename to Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs index 14dd3ef6e3c7..915f92a87bf9 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeHost.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json.Serialization; namespace Microsoft.AspNet.NodeServices { - internal class HttpNodeHost : OutOfProcessNodeRunner { + internal class HttpNodeInstance : OutOfProcessNodeInstance { private readonly static Regex PortMessageRegex = new Regex(@"^\[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port (\d+)\]$"); private readonly static JsonSerializerSettings jsonSerializerSettings = new JsonSerializerSettings { @@ -15,8 +15,8 @@ internal class HttpNodeHost : OutOfProcessNodeRunner { private int _portNumber; - public HttpNodeHost(int port = 0) - : base(EmbeddedResourceReader.Read(typeof(HttpNodeHost), "/Content/Node/entrypoint-http.js"), port.ToString()) + public HttpNodeInstance(int port = 0) + : base(EmbeddedResourceReader.Read(typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), port.ToString()) { } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs similarity index 93% rename from Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs rename to Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs index f8a7ca793882..0f799351d809 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeHost.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs @@ -15,7 +15,7 @@ namespace Microsoft.AspNet.NodeServices { // Instead of directly using stdin/stdout, we could use either regular sockets (TCP) or use named pipes // on Windows and domain sockets on Linux / OS X, but either way would need a system for framing the // requests, associating them with responses, and scheduling use of the comms channel. - internal class InputOutputStreamNodeHost : OutOfProcessNodeRunner + internal class InputOutputStreamNodeInstance : OutOfProcessNodeInstance { private SemaphoreSlim _invocationSemaphore = new SemaphoreSlim(1); private TaskCompletionSource _currentInvocationResult; @@ -24,8 +24,8 @@ internal class InputOutputStreamNodeHost : OutOfProcessNodeRunner ContractResolver = new CamelCasePropertyNamesContractResolver() }; - public InputOutputStreamNodeHost() - : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeHost), "/Content/Node/entrypoint-stream.js")) + public InputOutputStreamNodeInstance() + : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeInstance), "/Content/Node/entrypoint-stream.js")) { } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs b/Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs deleted file mode 100644 index a66f9787f2c6..000000000000 --- a/Microsoft.AspNet.NodeServices/HostingModels/NodeHost.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Threading.Tasks; - -namespace Microsoft.AspNet.NodeServices { - public abstract class NodeHost : System.IDisposable - { - public abstract Task Invoke(NodeInvocationInfo invocationInfo); - - public abstract void Dispose(); - } -} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs similarity index 85% rename from Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs rename to Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index fb3d06f962ce..5fa5e5379feb 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeRunner.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNet.NodeServices { * Class responsible for launching the Node child process, determining when it is ready to accept invocations, * and finally killing it when the parent process exits. Also it restarts the child process if it dies. */ - internal abstract class OutOfProcessNodeRunner : NodeHost { + public abstract class OutOfProcessNodeInstance : INodeServices { private object _childProcessLauncherLock; private bool disposed; private StringAsTempFile _entryPointScript; @@ -18,19 +18,33 @@ internal abstract class OutOfProcessNodeRunner : NodeHost { protected Process NodeProcess { get { - // This is only exposed to support the UnreliableStreamNodeHost, which is just to verify that + // This is only exposed to support the unreliable OutOfProcessNodeRunner, which is just to verify that // other hosting/transport mechanisms are possible. This shouldn't really be exposed. return this._nodeProcess; } } - public OutOfProcessNodeRunner(string entryPointScript, string commandLineArguments = null) + public OutOfProcessNodeInstance(string entryPointScript, string commandLineArguments = null) { this._childProcessLauncherLock = new object(); this._entryPointScript = new StringAsTempFile(entryPointScript); this._commandLineArguments = commandLineArguments ?? string.Empty; } + public abstract Task Invoke(NodeInvocationInfo invocationInfo); + + public Task Invoke(string moduleName, params object[] args) { + return this.InvokeExport(moduleName, null, args); + } + + public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { + return await this.Invoke(new NodeInvocationInfo { + ModuleName = moduleName, + ExportedFunctionName = exportedFunctionName, + Args = args + }); + } + protected async Task EnsureReady() { lock (this._childProcessLauncherLock) { if (this._nodeProcess == null || this._nodeProcess.HasExited) { @@ -104,8 +118,8 @@ protected virtual void OnOutputDataReceived(string outputData) { protected virtual void OnErrorDataReceived(string errorData) { Console.WriteLine("[Node] " + errorData); } - - public override void Dispose() + + public void Dispose() { Dispose(true); GC.SuppressFinalize(this); @@ -125,8 +139,8 @@ protected virtual void Dispose(bool disposing) disposed = true; } } - - ~OutOfProcessNodeRunner() { + + ~OutOfProcessNodeInstance() { Dispose (false); } } diff --git a/Microsoft.AspNet.NodeServices/INodeInstance.cs b/Microsoft.AspNet.NodeServices/INodeInstance.cs new file mode 100644 index 000000000000..9118688f8447 --- /dev/null +++ b/Microsoft.AspNet.NodeServices/INodeInstance.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices { + public interface INodeServices : IDisposable { + Task Invoke(string moduleName, params object[] args); + + Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); + } +} diff --git a/Microsoft.AspNet.NodeServices/NodeInstance.cs b/Microsoft.AspNet.NodeServices/NodeInstance.cs deleted file mode 100644 index 7768f18d1d23..000000000000 --- a/Microsoft.AspNet.NodeServices/NodeInstance.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace Microsoft.AspNet.NodeServices { - public class NodeInstance : IDisposable { - private readonly NodeHost _nodeHost; - - public NodeInstance(NodeHostingModel hostingModel = NodeHostingModel.Http) { - switch (hostingModel) { - case NodeHostingModel.Http: - this._nodeHost = new HttpNodeHost(); - break; - case NodeHostingModel.InputOutputStream: - this._nodeHost = new InputOutputStreamNodeHost(); - break; - default: - throw new ArgumentException("Unknown hosting model: " + hostingModel.ToString()); - } - } - - public Task Invoke(string moduleName, params object[] args) { - return this.InvokeExport(moduleName, null, args); - } - - public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { - return await this._nodeHost.Invoke(new NodeInvocationInfo { - ModuleName = moduleName, - ExportedFunctionName = exportedFunctionName, - Args = args - }); - } - - public void Dispose() - { - this._nodeHost.Dispose(); - } - } -} diff --git a/Microsoft.AspNet.NodeServices/HostingModels/EmbeddedResourceReader.cs b/Microsoft.AspNet.NodeServices/Util/EmbeddedResourceReader.cs similarity index 100% rename from Microsoft.AspNet.NodeServices/HostingModels/EmbeddedResourceReader.cs rename to Microsoft.AspNet.NodeServices/Util/EmbeddedResourceReader.cs diff --git a/Microsoft.AspNet.NodeServices/StringAsTempFile.cs b/Microsoft.AspNet.NodeServices/Util/StringAsTempFile.cs similarity index 100% rename from Microsoft.AspNet.NodeServices/StringAsTempFile.cs rename to Microsoft.AspNet.NodeServices/Util/StringAsTempFile.cs diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index d94a2107bf59..c071791f3229 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha1", + "version": "1.0.0-alpha2", "description": "Microsoft.AspNet.NodeServices", "authors": [ "Microsoft" ], "tags": [""], @@ -8,7 +8,8 @@ "dependencies": { "System.Net.Http": "4.0.1-beta-23409", - "Newtonsoft.Json": "8.0.1-beta1" + "Newtonsoft.Json": "8.0.1-beta1", + "Microsoft.Framework.DependencyInjection": "1.0.0-beta8" }, "frameworks": { diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index 89fb223e0ffd..592302e4129c 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -79,6 +79,9 @@ public void ConfigureServices(IServiceCollection services) Mapper.CreateMap(); Mapper.CreateMap(); Mapper.CreateMap(); + + // Enable Node Services + services.AddNodeServices(); } // Configure is called after ConfigureServices is called. @@ -108,17 +111,6 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseExceptionHandler("/Home/Error"); } - var nodeInstance = new NodeInstance(); - app.Use(async (context, next) => { - if (context.Request.Path.Value.EndsWith(".less")) { - // Note: check for directory traversal - var output = await nodeInstance.Invoke("lessCompiler.js", env.WebRootPath + context.Request.Path.Value); - await context.Response.WriteAsync(output); - } else { - await next(); - } - }); - // Add static files to the request pipeline. app.UseStaticFiles(); diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 7a9f18c604d6..76659e2108d2 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,7 +19,7 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha1" + "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha2" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/misc/ES2015Transpilation/Controllers/HomeController.cs b/samples/misc/ES2015Transpilation/Controllers/HomeController.cs index 9d9dd3e6b5b4..6ee3ef86a6c6 100755 --- a/samples/misc/ES2015Transpilation/Controllers/HomeController.cs +++ b/samples/misc/ES2015Transpilation/Controllers/HomeController.cs @@ -5,7 +5,7 @@ namespace ES2015Example.Controllers { public class HomeController : Controller { - public async Task Index(int pageIndex) + public IActionResult Index(int pageIndex) { return View(); } diff --git a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs index cfb1cbfc6c92..4b598be430ef 100644 --- a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs +++ b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs @@ -6,13 +6,17 @@ namespace ES2015Example.Controllers { public class ScriptController : Controller { - private static NodeInstance nodeInstance = new NodeInstance(); + private INodeServices nodeServices; + + public ScriptController(INodeServices nodeServices) { + this.nodeServices = nodeServices; + } public async Task Transpile(string filename) { // TODO: Don't hard-code wwwroot; use proper path conversions var fileContents = System.IO.File.ReadAllText("wwwroot/" + filename); - var transpiledResult = await nodeInstance.Invoke("transpilation.js", fileContents, Request.Path.Value); + var transpiledResult = await this.nodeServices.Invoke("transpilation.js", fileContents, Request.Path.Value); return Content(transpiledResult, "application/javascript"); } } diff --git a/samples/misc/ES2015Transpilation/Startup.cs b/samples/misc/ES2015Transpilation/Startup.cs index b3584db95a71..ea55a3f5b915 100755 --- a/samples/misc/ES2015Transpilation/Startup.cs +++ b/samples/misc/ES2015Transpilation/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.Framework.Configuration; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; +using Microsoft.AspNet.NodeServices; namespace ES2015Example { @@ -27,6 +28,9 @@ public void ConfigureServices(IServiceCollection services) { // Add MVC services to the services container. services.AddMvc(); + + // Enable Node Services + services.AddNodeServices(); } // Configure is called after ConfigureServices is called. diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json index f6542c94c0a9..16aa144154d0 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/ES2015Transpilation/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices": "1.0.0-alpha1" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha2" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/Controllers/HomeController.cs b/samples/react/ReactGrid/Controllers/HomeController.cs index dd9a1abfec28..37dff2bc963a 100755 --- a/samples/react/ReactGrid/Controllers/HomeController.cs +++ b/samples/react/ReactGrid/Controllers/HomeController.cs @@ -1,14 +1,21 @@ using System.Threading.Tasks; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.NodeServices; using Microsoft.AspNet.NodeServices.React; namespace ReactExample.Controllers { public class HomeController : Controller { + private INodeServices nodeServices; + + public HomeController(INodeServices nodeServices) { + this.nodeServices = nodeServices; + } + public async Task Index(int pageIndex) { - ViewData["ReactOutput"] = await ReactRenderer.RenderToString( + ViewData["ReactOutput"] = await ReactRenderer.RenderToString(this.nodeServices, moduleName: "ReactApp/components/ReactApp.jsx", exportName: "ReactApp", baseUrl: Request.Path diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs index 46ddd6035438..9a3a6c99d685 100755 --- a/samples/react/ReactGrid/Startup.cs +++ b/samples/react/ReactGrid/Startup.cs @@ -1,5 +1,6 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.NodeServices; using Microsoft.Dnx.Runtime; using Microsoft.Framework.Configuration; using Microsoft.Framework.DependencyInjection; @@ -26,6 +27,9 @@ public void ConfigureServices(IServiceCollection services) { // Add MVC services to the services container. services.AddMvc(); + + // Enable Node Services + services.AddNodeServices(); } // Configure is called after ConfigureServices is called. diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index 5a346c885f0f..5c33b46d1a74 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha1" + "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha2" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" From 37eb4efff048d085ad0a4754224e2091019f1f98 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 14:52:45 -0800 Subject: [PATCH 0008/1585] Remove 'express' and 'body-parser' NPM dependencies from HttpNodeInstance --- .../Content/Node/entrypoint-http.js | 71 ++++++++++--------- samples/angular/MusicStore/package.json | 3 - samples/misc/ES2015Transpilation/package.json | 4 +- samples/react/ReactGrid/package.json | 2 - 4 files changed, 39 insertions(+), 41 deletions(-) diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js index 35ed458804bf..16d2a7729166 100644 --- a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -1,50 +1,55 @@ +// Limit dependencies to core Node modules. This means the code in this file has to be very low-level and unattractive, +// but simplifies things for the consumer of this module. +var http = require('http'); var path = require('path'); -var express = require('express'); -var bodyParser = require('body-parser') var requestedPortOrZero = parseInt(process.argv[2]) || 0; // 0 means 'let the OS decide' autoQuitOnFileChange(process.cwd(), ['.js', '.json', '.html']); -var app = express(); -app.use(bodyParser.json()); - -app.all('/', function (req, res) { - var resolvedPath = path.resolve(process.cwd(), req.body.moduleName); - var invokedModule = require(resolvedPath); - var func = req.body.exportedFunctionName ? invokedModule[req.body.exportedFunctionName] : invokedModule; - if (!func) { - throw new Error('The module "' + resolvedPath + '" has no export named "' + req.body.exportedFunctionName + '"'); - } - - var hasSentResult = false; - var callback = function(errorValue, successValue) { - if (!hasSentResult) { - hasSentResult = true; - if (errorValue) { - res.status(500).send(errorValue); - } else { - sendResult(res, successValue); - } +var server = http.createServer(function(req, res) { + readRequestBodyAsJson(req, function(bodyJson) { + var resolvedPath = path.resolve(process.cwd(), bodyJson.moduleName); + var invokedModule = require(resolvedPath); + var func = bodyJson.exportedFunctionName ? invokedModule[bodyJson.exportedFunctionName] : invokedModule; + if (!func) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"'); } - }; - - func.apply(null, [callback].concat(req.body.args)); + + var hasSentResult = false; + var callback = function(errorValue, successValue) { + if (!hasSentResult) { + hasSentResult = true; + if (errorValue) { + res.status(500).send(errorValue); + } else if (typeof successValue === 'object') { + // Arbitrary object - JSON-serialize it + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify(successValue)); + } else { + // String - can bypass JSON-serialization altogether + res.setHeader('Content-Type', 'text/plain'); + res.end(successValue); + } + } + }; + + func.apply(null, [callback].concat(bodyJson.args)); + }); }); -var listener = app.listen(requestedPortOrZero, 'localhost', function () { +server.listen(requestedPortOrZero, 'localhost', function () { // Signal to HttpNodeHost which port it should make its HTTP connections on - console.log('[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port ' + listener.address().port + '\]'); + console.log('[Microsoft.AspNet.NodeServices.HttpNodeHost:Listening on port ' + server.address().port + '\]'); // Signal to the NodeServices base class that we're ready to accept invocations console.log('[Microsoft.AspNet.NodeServices:Listening]'); }); -function sendResult(response, result) { - if (typeof result === 'object') { - response.json(result); - } else { - response.send(result); - } +function readRequestBodyAsJson(request, callback) { + var requestBodyAsString = ''; + request + .on('data', function(chunk) { requestBodyAsString += chunk; }) + .on('end', function() { callback(JSON.parse(requestBodyAsString)); }); } function autoQuitOnFileChange(rootDir, extensions) { diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json index f1db973e9169..0925c4fb46ad 100644 --- a/samples/angular/MusicStore/package.json +++ b/samples/angular/MusicStore/package.json @@ -4,11 +4,8 @@ "dependencies": { "angular2": "2.0.0-alpha.44", "angular2-universal-patched": "^0.5.4", - "body-parser": "^1.14.1", "bootstrap": "^3.3.5", - "del": "^2.0.2", "es6-module-loader": "^0.15.0", - "express": "^4.13.3", "jquery": "^2.1.4", "less": "^2.5.3", "reflect-metadata": "^0.1.2", diff --git a/samples/misc/ES2015Transpilation/package.json b/samples/misc/ES2015Transpilation/package.json index c3436cf53a91..0e21db665ade 100644 --- a/samples/misc/ES2015Transpilation/package.json +++ b/samples/misc/ES2015Transpilation/package.json @@ -2,8 +2,6 @@ "name": "ES2015Example", "version": "0.0.0", "dependencies": { - "babel-core": "^5.8.29", - "body-parser": "^1.14.1", - "express": "^4.13.3" + "babel-core": "^5.8.29" } } diff --git a/samples/react/ReactGrid/package.json b/samples/react/ReactGrid/package.json index 86bf49d22341..2249b9171364 100644 --- a/samples/react/ReactGrid/package.json +++ b/samples/react/ReactGrid/package.json @@ -3,9 +3,7 @@ "version": "0.0.0", "dependencies": { "babel-core": "^5.8.29", - "body-parser": "^1.14.1", "bootstrap": "^3.3.5", - "express": "^4.13.3", "griddle-react": "^0.2.14", "history": "^1.12.6", "react": "^0.14.0", From 7e1955c6fe9f37f92b2a59af2f4f7f1ab30f8110 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 20:07:31 -0800 Subject: [PATCH 0009/1585] Make app.AddNodeServices optional in Angular case. Fix tag helper attribute name. Bump versions. --- .../AngularPrerenderTagHelper.cs | 17 ++++++++++++----- .../project.json | 4 ++-- .../project.json | 4 ++-- Microsoft.AspNet.NodeServices/Configuration.cs | 2 +- Microsoft.AspNet.NodeServices/project.json | 2 +- samples/angular/MusicStore/Startup.cs | 4 ---- .../angular/MusicStore/Views/Home/Index.cshtml | 2 +- samples/angular/MusicStore/project.json | 2 +- samples/misc/ES2015Transpilation/project.json | 2 +- samples/react/ReactGrid/project.json | 2 +- 10 files changed, 22 insertions(+), 19 deletions(-) diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs index 31e4602c40ae..d920a1797f67 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs @@ -1,7 +1,7 @@ +using System; using System.Threading.Tasks; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; -using Microsoft.AspNet.NodeServices; using Microsoft.AspNet.Http.Extensions; namespace Microsoft.AspNet.NodeServices.Angular @@ -10,6 +10,7 @@ namespace Microsoft.AspNet.NodeServices.Angular public class AngularRunAtServerTagHelper : TagHelper { static StringAsTempFile nodeScript; + static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI static AngularRunAtServerTagHelper() { // Consider populating this lazily @@ -17,8 +18,8 @@ static AngularRunAtServerTagHelper() { nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit } - const string PrerenderModuleAttributeName = "aspnet-ng2-prerender-module"; - const string PrerenderExportAttributeName = "aspnet-ng2-prerender-export"; + const string PrerenderModuleAttributeName = "asp-ng2-prerender-module"; + const string PrerenderExportAttributeName = "asp-ng2-prerender-export"; [HtmlAttributeName(PrerenderModuleAttributeName)] public string ModuleName { get; set; } @@ -29,10 +30,16 @@ static AngularRunAtServerTagHelper() { private IHttpContextAccessor contextAccessor; private INodeServices nodeServices; - public AngularRunAtServerTagHelper(INodeServices nodeServices, IHttpContextAccessor contextAccessor) + public AngularRunAtServerTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; - this.nodeServices = nodeServices; + this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; + + // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() + // in your startup file, but then again it might be confusing that you don't need to. + if (this.nodeServices == null) { + this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http); + } } public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json index fec49f8b24ca..241590e214ad 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha2", + "version": "1.0.0-alpha3", "description": "Microsoft.AspNet.NodeServices.Angular Class Library", "authors": [ "Microsoft" @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha2", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha3", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" }, "resource": [ diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json index a0998400a205..36356cfbf7cd 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha2", + "version": "1.0.0-alpha3", "description": "Microsoft.AspNet.NodeServices.React Class Library", "authors": [ "Microsoft" @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha2" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha3" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.NodeServices/Configuration.cs b/Microsoft.AspNet.NodeServices/Configuration.cs index c0c336e7ea37..2323812d42ba 100644 --- a/Microsoft.AspNet.NodeServices/Configuration.cs +++ b/Microsoft.AspNet.NodeServices/Configuration.cs @@ -8,7 +8,7 @@ public static void AddNodeServices(this IServiceCollection serviceCollection, No }); } - private static INodeServices CreateNodeServices(NodeHostingModel hostingModel) + public static INodeServices CreateNodeServices(NodeHostingModel hostingModel) { switch (hostingModel) { diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index c071791f3229..81a7a236589c 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha2", + "version": "1.0.0-alpha3", "description": "Microsoft.AspNet.NodeServices", "authors": [ "Microsoft" ], "tags": [""], diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index 592302e4129c..81a5279c4c70 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -8,7 +8,6 @@ using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity.EntityFramework; -using Microsoft.AspNet.NodeServices; using Microsoft.Data.Entity; using Microsoft.Dnx.Runtime; using Microsoft.Framework.Configuration; @@ -79,9 +78,6 @@ public void ConfigureServices(IServiceCollection services) Mapper.CreateMap(); Mapper.CreateMap(); Mapper.CreateMap(); - - // Enable Node Services - services.AddNodeServices(); } // Configure is called after ConfigureServices is called. diff --git a/samples/angular/MusicStore/Views/Home/Index.cshtml b/samples/angular/MusicStore/Views/Home/Index.cshtml index 901925158f5a..acd6a30a3566 100755 --- a/samples/angular/MusicStore/Views/Home/Index.cshtml +++ b/samples/angular/MusicStore/Views/Home/Index.cshtml @@ -3,7 +3,7 @@ } - + Loading... diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 76659e2108d2..456822603b7e 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,7 +19,7 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha2" + "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha3" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json index 16aa144154d0..d8be5db0c675 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/ES2015Transpilation/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices": "1.0.0-alpha2" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha3" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index 5c33b46d1a74..5d6f8e0923df 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha2" + "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha3" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" From 0c59f670b2954046b83a099761842f3fd65a3f4a Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 20:23:05 -0800 Subject: [PATCH 0010/1585] Simplify ReactRenderer API when referencing default modules --- .../Content/Node/react-rendering.js | 2 +- Microsoft.AspNet.NodeServices.React/ReactRenderer.cs | 4 ++++ samples/react/ReactGrid/Controllers/HomeController.cs | 1 - samples/react/ReactGrid/ReactApp/boot-client.jsx | 2 +- samples/react/ReactGrid/ReactApp/components/ReactApp.jsx | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js index 27e97c7b9c29..b60e2d970bc0 100644 --- a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js +++ b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js @@ -27,7 +27,7 @@ module.exports = { renderToString: function(callback, options) { var resolvedPath = path.resolve(process.cwd(), options.moduleName); var requestedModule = require(resolvedPath); - var component = requestedModule[options.exportName]; + var component = options.exportName ? requestedModule[options.exportName] : requestedModule; if (!component) { throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"'); } diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs index 4094a2a43742..86e0794d4236 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs +++ b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs @@ -12,6 +12,10 @@ static ReactRenderer() { nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit } + public static Task RenderToString(INodeServices nodeServices, string moduleName, string baseUrl) { + return RenderToString(nodeServices, moduleName, /* exportName */ null, baseUrl); + } + public static async Task RenderToString(INodeServices nodeServices, string moduleName, string exportName, string baseUrl) { return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { moduleName, diff --git a/samples/react/ReactGrid/Controllers/HomeController.cs b/samples/react/ReactGrid/Controllers/HomeController.cs index 37dff2bc963a..101330b8e6aa 100755 --- a/samples/react/ReactGrid/Controllers/HomeController.cs +++ b/samples/react/ReactGrid/Controllers/HomeController.cs @@ -17,7 +17,6 @@ public async Task Index(int pageIndex) { ViewData["ReactOutput"] = await ReactRenderer.RenderToString(this.nodeServices, moduleName: "ReactApp/components/ReactApp.jsx", - exportName: "ReactApp", baseUrl: Request.Path ); return View(); diff --git a/samples/react/ReactGrid/ReactApp/boot-client.jsx b/samples/react/ReactGrid/ReactApp/boot-client.jsx index 623daf48fb8d..42e4f9012718 100644 --- a/samples/react/ReactGrid/ReactApp/boot-client.jsx +++ b/samples/react/ReactGrid/ReactApp/boot-client.jsx @@ -1,7 +1,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import createBrowserHistory from 'history/lib/createBrowserHistory'; -import { ReactApp } from './components/ReactApp.jsx'; +import ReactApp from './components/ReactApp.jsx'; // In the browser, we render into a DOM node and hook up to the browser's history APIs var history = createBrowserHistory(); diff --git a/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx b/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx index 45c9bf11aa93..f135152bb560 100644 --- a/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx +++ b/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { Router, Route } from 'react-router'; import { PeopleGrid } from './PeopleGrid.jsx'; -export class ReactApp extends React.Component { +export default class ReactApp extends React.Component { render() { return ( From e410affbd86f79b4c5d05f8911fad217dac5e9c2 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 2 Nov 2015 21:02:47 -0800 Subject: [PATCH 0011/1585] Switch ES2015 example to use middleware inlined into Startup.cs instead of MVC controller/action --- .../Controllers/ScriptController.cs | 23 ----------------- samples/misc/ES2015Transpilation/Startup.cs | 25 ++++++++++++------- .../misc/ES2015Transpilation/transpilation.js | 8 +++--- 3 files changed, 21 insertions(+), 35 deletions(-) delete mode 100644 samples/misc/ES2015Transpilation/Controllers/ScriptController.cs diff --git a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs b/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs deleted file mode 100644 index 4b598be430ef..000000000000 --- a/samples/misc/ES2015Transpilation/Controllers/ScriptController.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.NodeServices; - -namespace ES2015Example.Controllers -{ - public class ScriptController : Controller - { - private INodeServices nodeServices; - - public ScriptController(INodeServices nodeServices) { - this.nodeServices = nodeServices; - } - - public async Task Transpile(string filename) - { - // TODO: Don't hard-code wwwroot; use proper path conversions - var fileContents = System.IO.File.ReadAllText("wwwroot/" + filename); - var transpiledResult = await this.nodeServices.Invoke("transpilation.js", fileContents, Request.Path.Value); - return Content(transpiledResult, "application/javascript"); - } - } -} diff --git a/samples/misc/ES2015Transpilation/Startup.cs b/samples/misc/ES2015Transpilation/Startup.cs index ea55a3f5b915..a95bd1abd429 100755 --- a/samples/misc/ES2015Transpilation/Startup.cs +++ b/samples/misc/ES2015Transpilation/Startup.cs @@ -1,11 +1,11 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.Routing.Template; using Microsoft.Dnx.Runtime; using Microsoft.Framework.Configuration; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; using Microsoft.AspNet.NodeServices; +using Microsoft.AspNet.Http; namespace ES2015Example { @@ -34,7 +34,7 @@ public void ConfigureServices(IServiceCollection services) } // Configure is called after ConfigureServices is called. - public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) + public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, INodeServices nodeServices) { loggerFactory.MinimumLevel = LogLevel.Information; loggerFactory.AddConsole(); @@ -57,13 +57,20 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF app.UseExceptionHandler("/Home/Error"); } - app.UseMvc(routes => { - routes.MapRoute( - name: "Script", - template: "{*filename}", - defaults: new { controller="Script", action="Transpile" }, - constraints: new { filename = @"js/(.*?)\.js" } - ); + // Dynamically transpile any .js files under the '/js/' directory + app.Use(next => async context => { + var requestPath = context.Request.Path.Value; + if (requestPath.StartsWith("/js/") && requestPath.EndsWith(".js")) { + var fileInfo = env.WebRootFileProvider.GetFileInfo(requestPath); + if (fileInfo.Exists) { + var transpiled = await nodeServices.Invoke("transpilation.js", fileInfo.PhysicalPath, requestPath); + await context.Response.WriteAsync(transpiled); + return; + } + } + + // Not a JS file, or doesn't exist - let some other middleware handle it + await next.Invoke(context); }); // Add static files to the request pipeline. diff --git a/samples/misc/ES2015Transpilation/transpilation.js b/samples/misc/ES2015Transpilation/transpilation.js index ac239e580883..972a4d7f93c0 100644 --- a/samples/misc/ES2015Transpilation/transpilation.js +++ b/samples/misc/ES2015Transpilation/transpilation.js @@ -1,9 +1,11 @@ +var fs = require('fs'); var babelCore = require('babel-core'); -module.exports = function(cb, fileContents, url) { - var result = babelCore.transform(fileContents, { +module.exports = function(cb, physicalPath, requestPath) { + var originalContents = fs.readFileSync(physicalPath); + var result = babelCore.transform(originalContents, { sourceMaps: 'inline', - sourceFileName: '/sourcemapped/' + url + sourceFileName: '/sourcemapped' + requestPath }); cb(null, result.code); } From 7c3d22c7b6feba745f95a4e33d2aa43278f929a4 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 4 Nov 2015 12:19:43 -0800 Subject: [PATCH 0012/1585] Add react tag helper. Clean up code and make it more consistent. --- .../AngularPrerenderTagHelper.cs | 24 ++++----- .../AngularRenderer.cs | 24 +++++++++ .../Content/Node/angular-rendering.js | 39 +++++++++++---- .../project.json | 2 +- .../Content/Node/react-rendering.js | 30 +++++++++--- .../ReactPrerenderTagHelper.cs | 49 +++++++++++++++++++ .../ReactRenderer.cs | 12 ++--- .../project.json | 5 +- samples/angular/MusicStore/project.json | 2 +- .../ReactGrid/Controllers/HomeController.cs | 14 +----- samples/react/ReactGrid/Startup.cs | 3 -- .../react/ReactGrid/Views/Home/Index.cshtml | 2 +- .../react/ReactGrid/Views/_ViewImports.cshtml | 1 + samples/react/ReactGrid/project.json | 2 +- 14 files changed, 146 insertions(+), 63 deletions(-) create mode 100644 Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs create mode 100644 Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs index d920a1797f67..0de90ea9f217 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs @@ -7,17 +7,10 @@ namespace Microsoft.AspNet.NodeServices.Angular { [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] - public class AngularRunAtServerTagHelper : TagHelper + public class AngularPrerenderTagHelper : TagHelper { - static StringAsTempFile nodeScript; static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI - static AngularRunAtServerTagHelper() { - // Consider populating this lazily - var script = EmbeddedResourceReader.Read(typeof (AngularRunAtServerTagHelper), "/Content/Node/angular-rendering.js"); - nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit - } - const string PrerenderModuleAttributeName = "asp-ng2-prerender-module"; const string PrerenderExportAttributeName = "asp-ng2-prerender-export"; @@ -30,7 +23,7 @@ static AngularRunAtServerTagHelper() { private IHttpContextAccessor contextAccessor; private INodeServices nodeServices; - public AngularRunAtServerTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) + public AngularPrerenderTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; @@ -44,12 +37,13 @@ public AngularRunAtServerTagHelper(IServiceProvider nodeServices, IHttpContextAc public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) { - var result = await this.nodeServices.InvokeExport(nodeScript.FileName, "renderComponent", new { - componentModule = this.ModuleName, - componentExport = this.ExportName, - tagName = output.TagName, - baseUrl = UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request) - }); + var result = await AngularRenderer.RenderToString( + nodeServices: this.nodeServices, + componentModuleName: this.ModuleName, + componentExportName: this.ExportName, + componentTagName: output.TagName, + requestUrl: UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request) + ); output.SuppressOutput(); output.PostElement.AppendEncoded(result); } diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs b/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs new file mode 100644 index 000000000000..4ca5868857fe --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; + +namespace Microsoft.AspNet.NodeServices.Angular +{ + public static class AngularRenderer + { + private static StringAsTempFile nodeScript; + + static AngularRenderer() { + // Consider populating this lazily + var script = EmbeddedResourceReader.Read(typeof (AngularRenderer), "/Content/Node/angular-rendering.js"); + nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit + } + + public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string componentTagName, string requestUrl) { + return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { + moduleName = componentModuleName, + exportName = componentExportName, + tagName = componentTagName, + requestUrl = requestUrl + }); + } + } +} diff --git a/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js b/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js index 57b7df836980..460400e7e30d 100644 --- a/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js +++ b/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js @@ -3,20 +3,39 @@ var ngUniversal = require('angular2-universal-patched'); var ng = require('angular2/angular2'); var ngRouter = require('angular2/router'); -module.exports = { - renderComponent: function(callback, options) { - // Find the component class. Use options.componentExport if specified, otherwise convert tag-name to PascalCase. - var loadedModule = require(path.resolve(process.cwd(), options.componentModule)); - var componentExport = options.componentExport || options.tagName.replace(/(-|^)([a-z])/g, function (m1, m2, char) { return char.toUpperCase(); }); - var component = loadedModule[componentExport]; - if (!component) { - throw new Error('The module "' + options.componentModule + '" has no export named "' + componentExport + '"'); - } +function getExportOrThrow(moduleInstance, moduleFilename, exportName) { + if (!(exportName in moduleInstance)) { + throw new Error('The module "' + moduleFilename + '" has no export named "' + exportName + '"'); + } + return moduleInstance[exportName]; +} +function findAngularComponent(options) { + var resolvedPath = path.resolve(process.cwd(), options.moduleName); + var loadedModule = require(resolvedPath); + if (options.exportName) { + // If exportName is specified explicitly, use it + return getExportOrThrow(loadedModule, resolvedPath, options.exportName); + } else if (typeof loadedModule === 'function') { + // Otherwise, if the module itself is a function, assume that is the component + return loadedModule; + } else if (typeof loadedModule.default === 'function') { + // Otherwise, if the module has a default export which is a function, assume that is the component + return loadedModule.default; + } else { + // Otherwise, guess the export name by converting tag-name to PascalCase + var tagNameAsPossibleExport = options.tagName.replace(/(-|^)([a-z])/g, function (m1, m2, char) { return char.toUpperCase(); }); + return getExportOrThrow(loadedModule, resolvedPath, tagNameAsPossibleExport); + } +} + +module.exports = { + renderToString: function(callback, options) { + var component = findAngularComponent(options); var serverBindings = [ ngRouter.ROUTER_BINDINGS, ngUniversal.HTTP_PROVIDERS, - ng.provide(ngUniversal.BASE_URL, { useValue: options.baseUrl }), + ng.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }), ngUniversal.SERVER_LOCATION_PROVIDERS ]; diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json index 241590e214ad..a26621040e5e 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha3", + "version": "1.0.0-alpha4", "description": "Microsoft.AspNet.NodeServices.Angular Class Library", "authors": [ "Microsoft" diff --git a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js index b60e2d970bc0..74db4836089e 100644 --- a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js +++ b/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js @@ -10,6 +10,26 @@ var origJsLoader = require.extensions['.js']; require.extensions['.js'] = loadViaBabel; require.extensions['.jsx'] = loadViaBabel; +function findReactComponent(options) { + var resolvedPath = path.resolve(process.cwd(), options.moduleName); + var loadedModule = require(resolvedPath); + if (options.exportName) { + // If exportName is specified explicitly, use it + if (!(options.exportName in loadedModule)) { + throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"'); + } + return loadedModule[options.exportName]; + } else if (typeof loadedModule === 'function') { + // Otherwise, if the module itself is a function, assume that is the component + return loadedModule; + } else if (typeof loadedModule.default === 'function') { + // Otherwise, if the module has a default export which is a function, assume that is the component + return loadedModule.default; + } else { + throw new Error('Cannot find React component, because no export name was specified, and the module "' + resolvedPath + '" has no default exported class.'); + } +} + function loadViaBabel(module, filename) { // Assume that all the app's own code is ES2015+ (optionally with JSX), but that none of the node_modules are. // The distinction is important because ES2015+ forces strict mode, and it may break ES3/5 if you try to run it in strict @@ -25,14 +45,8 @@ function loadViaBabel(module, filename) { module.exports = { renderToString: function(callback, options) { - var resolvedPath = path.resolve(process.cwd(), options.moduleName); - var requestedModule = require(resolvedPath); - var component = options.exportName ? requestedModule[options.exportName] : requestedModule; - if (!component) { - throw new Error('The module "' + resolvedPath + '" has no export named "' + options.exportName + '"'); - } - - var history = createMemoryHistory(options.baseUrl); + var component = findReactComponent(options); + var history = createMemoryHistory(options.requestUrl); var reactElement = React.createElement(component, { history: history }); var html = ReactDOMServer.renderToString(reactElement); callback(null, html); diff --git a/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs new file mode 100644 index 000000000000..7ca084ac3279 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNet.Razor.Runtime.TagHelpers; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Extensions; + +namespace Microsoft.AspNet.NodeServices.React +{ + [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] + public class ReactPrerenderTagHelper : TagHelper + { + static INodeServices fallbackNodeServices; // Used only if no INodeServices was registered with DI + + const string PrerenderModuleAttributeName = "asp-react-prerender-module"; + const string PrerenderExportAttributeName = "asp-react-prerender-export"; + + [HtmlAttributeName(PrerenderModuleAttributeName)] + public string ModuleName { get; set; } + + [HtmlAttributeName(PrerenderExportAttributeName)] + public string ExportName { get; set; } + + private IHttpContextAccessor contextAccessor; + private INodeServices nodeServices; + + public ReactPrerenderTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) + { + this.contextAccessor = contextAccessor; + this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; + + // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() + // in your startup file, but then again it might be confusing that you don't need to. + if (this.nodeServices == null) { + this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http); + } + } + + public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output) + { + var request = this.contextAccessor.HttpContext.Request; + var result = await ReactRenderer.RenderToString( + nodeServices: this.nodeServices, + componentModuleName: this.ModuleName, + componentExportName: this.ExportName, + requestUrl: request.Path + request.QueryString.Value); + output.Content.SetContentEncoded(result); + } + } +} diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs index 86e0794d4236..d48a694f7a82 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs +++ b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs @@ -12,15 +12,11 @@ static ReactRenderer() { nodeScript = new StringAsTempFile(script); // Will be cleaned up on process exit } - public static Task RenderToString(INodeServices nodeServices, string moduleName, string baseUrl) { - return RenderToString(nodeServices, moduleName, /* exportName */ null, baseUrl); - } - - public static async Task RenderToString(INodeServices nodeServices, string moduleName, string exportName, string baseUrl) { + public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string requestUrl) { return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { - moduleName, - exportName, - baseUrl + moduleName = componentModuleName, + exportName = componentExportName, + requestUrl = requestUrl }); } } diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json index 36356cfbf7cd..6fa226dcaede 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha3", + "version": "1.0.0-alpha4", "description": "Microsoft.AspNet.NodeServices.React Class Library", "authors": [ "Microsoft" @@ -25,7 +25,8 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha3", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" }, "resource": [ "Content/**/*" diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 456822603b7e..94df1a6e093b 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,7 +19,7 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha4" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/Controllers/HomeController.cs b/samples/react/ReactGrid/Controllers/HomeController.cs index 101330b8e6aa..4a9899cdc446 100755 --- a/samples/react/ReactGrid/Controllers/HomeController.cs +++ b/samples/react/ReactGrid/Controllers/HomeController.cs @@ -1,24 +1,12 @@ using System.Threading.Tasks; using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.NodeServices; -using Microsoft.AspNet.NodeServices.React; namespace ReactExample.Controllers { public class HomeController : Controller { - private INodeServices nodeServices; - - public HomeController(INodeServices nodeServices) { - this.nodeServices = nodeServices; - } - - public async Task Index(int pageIndex) + public IActionResult Index(int pageIndex) { - ViewData["ReactOutput"] = await ReactRenderer.RenderToString(this.nodeServices, - moduleName: "ReactApp/components/ReactApp.jsx", - baseUrl: Request.Path - ); return View(); } diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs index 9a3a6c99d685..48ccde8bfb5b 100755 --- a/samples/react/ReactGrid/Startup.cs +++ b/samples/react/ReactGrid/Startup.cs @@ -27,9 +27,6 @@ public void ConfigureServices(IServiceCollection services) { // Add MVC services to the services container. services.AddMvc(); - - // Enable Node Services - services.AddNodeServices(); } // Configure is called after ConfigureServices is called. diff --git a/samples/react/ReactGrid/Views/Home/Index.cshtml b/samples/react/ReactGrid/Views/Home/Index.cshtml index bef6868f48af..76f8a6c5fb78 100755 --- a/samples/react/ReactGrid/Views/Home/Index.cshtml +++ b/samples/react/ReactGrid/Views/Home/Index.cshtml @@ -1,4 +1,4 @@ -
@Html.Raw(ViewData["ReactOutput"])
+
@section scripts { diff --git a/samples/react/ReactGrid/Views/_ViewImports.cshtml b/samples/react/ReactGrid/Views/_ViewImports.cshtml index 7839f6c9ff9b..8237608fdcfa 100755 --- a/samples/react/ReactGrid/Views/_ViewImports.cshtml +++ b/samples/react/ReactGrid/Views/_ViewImports.cshtml @@ -1,2 +1,3 @@ @using ReactExample @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" +@addTagHelper "*, Microsoft.AspNet.NodeServices.React" diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index 5d6f8e0923df..ed9de5ec409f 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha4" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" From 3e7dfa617a1cd0e211b337a99ace8d49268511b7 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 5 Nov 2015 09:17:46 -0800 Subject: [PATCH 0013/1585] Update version of base package to -alpha4 --- Microsoft.AspNet.NodeServices.Angular/project.json | 2 +- Microsoft.AspNet.NodeServices.React/project.json | 2 +- Microsoft.AspNet.NodeServices/project.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json index a26621040e5e..18a0e91273ec 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha3", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha4", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" }, "resource": [ diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json index 6fa226dcaede..c0c6b0399e56 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha3", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha4", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" }, "resource": [ diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index 81a7a236589c..1c30addaddc1 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha3", + "version": "1.0.0-alpha4", "description": "Microsoft.AspNet.NodeServices", "authors": [ "Microsoft" ], "tags": [""], From b5fb560c541fb8015c77a0138f4cd70010e182b5 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 5 Nov 2015 10:22:39 -0800 Subject: [PATCH 0014/1585] Require lodash (works around NPM dependency issue on Windows) --- samples/angular/MusicStore/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json index 0925c4fb46ad..251ee3396857 100644 --- a/samples/angular/MusicStore/package.json +++ b/samples/angular/MusicStore/package.json @@ -8,6 +8,7 @@ "es6-module-loader": "^0.15.0", "jquery": "^2.1.4", "less": "^2.5.3", + "lodash": "^3.10.1", "reflect-metadata": "^0.1.2", "systemjs": "^0.19.3", "traceur": "0.0.91" From a4c4e20d4e49926219feb42f0909c7a28531d7d2 Mon Sep 17 00:00:00 2001 From: Ricardo Peres Date: Thu, 5 Nov 2015 10:41:24 -0800 Subject: [PATCH 0015/1585] Restored Killed console message Added Started console message Captured possible exception from node process --- .../HostingModels/OutOfProcessNodeInstance.cs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 5fa5e5379feb..18951a42701e 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -15,7 +15,7 @@ public abstract class OutOfProcessNodeInstance : INodeServices { private string _commandLineArguments; private Process _nodeProcess; private TaskCompletionSource _nodeProcessIsReadySource; - + protected Process NodeProcess { get { // This is only exposed to support the unreliable OutOfProcessNodeRunner, which is just to verify that @@ -23,20 +23,20 @@ protected Process NodeProcess { return this._nodeProcess; } } - + public OutOfProcessNodeInstance(string entryPointScript, string commandLineArguments = null) { this._childProcessLauncherLock = new object(); this._entryPointScript = new StringAsTempFile(entryPointScript); this._commandLineArguments = commandLineArguments ?? string.Empty; } - + public abstract Task Invoke(NodeInvocationInfo invocationInfo); - + public Task Invoke(string moduleName, params object[] args) { return this.InvokeExport(moduleName, null, args); } - + public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { return await this.Invoke(new NodeInvocationInfo { ModuleName = moduleName, @@ -44,7 +44,7 @@ public async Task InvokeExport(string moduleName, string exportedFunctio Args = args }); } - + protected async Task EnsureReady() { lock (this._childProcessLauncherLock) { if (this._nodeProcess == null || this._nodeProcess.HasExited) { @@ -55,36 +55,44 @@ protected async Task EnsureReady() { RedirectStandardOutput = true, RedirectStandardError = true }; - + // Append current directory to NODE_PATH so it can locate node_modules var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; if (existingNodePath != string.Empty) { existingNodePath += ":"; } - + var nodePathValue = existingNodePath + Path.Combine(Directory.GetCurrentDirectory(), "node_modules"); #if DNX451 startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue); #else startInfo.Environment.Add("NODE_PATH", nodePathValue); #endif - + this.OnBeforeLaunchProcess(); this._nodeProcess = Process.Start(startInfo); this.ConnectToInputOutputStreams(); } } - - var initializationSucceeded = await this._nodeProcessIsReadySource.Task; + + var task = this._nodeProcessIsReadySource.Task; + + var initializationSucceeded = task + .GetAwaiter() + .GetResult(); + if (!initializationSucceeded) { - throw new InvalidOperationException("The Node.js process failed to initialize"); + throw new InvalidOperationException("The Node.js process failed to initialize", task.Exception); + } + else { + Console.WriteLine("Started"); } } - + private void ConnectToInputOutputStreams() { var initializationIsCompleted = false; // TODO: Make this thread-safe? (Interlocked.Exchange etc.) this._nodeProcessIsReadySource = new TaskCompletionSource(); - + this._nodeProcess.OutputDataReceived += (sender, evt) => { if (evt.Data == "[Microsoft.AspNet.NodeServices:Listening]" && !initializationIsCompleted) { this._nodeProcessIsReadySource.SetResult(true); @@ -103,18 +111,18 @@ private void ConnectToInputOutputStreams() { } } }; - + this._nodeProcess.BeginOutputReadLine(); - this._nodeProcess.BeginErrorReadLine(); + this._nodeProcess.BeginErrorReadLine(); } - + protected virtual void OnBeforeLaunchProcess() { } protected virtual void OnOutputDataReceived(string outputData) { Console.WriteLine("[Node] " + outputData); } - + protected virtual void OnErrorDataReceived(string errorData) { Console.WriteLine("[Node] " + errorData); } @@ -124,18 +132,19 @@ public void Dispose() Dispose(true); GC.SuppressFinalize(this); } - + protected virtual void Dispose(bool disposing) { if (!disposed) { if (disposing) { this._entryPointScript.Dispose(); } - + if (this._nodeProcess != null && !this._nodeProcess.HasExited) { - this._nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? System.Console.WriteLine("Killed"); + this._nodeProcess.Kill(); // TODO: Is there a more graceful way to end it? Or does this still let it perform any cleanup? + System.Console.WriteLine("Killed"); } - + disposed = true; } } From 46dc74317729c19a96ddf4f0cf6f80fd1b53b2c2 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 5 Nov 2015 11:46:10 -0800 Subject: [PATCH 0016/1585] Fix various path issues --- .../AngularPrerenderTagHelper.cs | 8 +++--- .../project.json | 7 ++--- .../ReactPrerenderTagHelper.cs | 8 +++--- .../project.json | 7 ++--- .../Configuration.cs | 10 ++++--- .../HostingModels/HttpNodeInstance.cs | 4 +-- .../InputOutputStreamNodeInstance.cs | 4 +-- .../HostingModels/OutOfProcessNodeInstance.cs | 10 ++++--- Microsoft.AspNet.NodeServices/project.json | 26 ++++++++++--------- samples/angular/MusicStore/project.json | 3 ++- samples/misc/ES2015Transpilation/project.json | 2 +- samples/react/ReactGrid/project.json | 2 +- 12 files changed, 52 insertions(+), 39 deletions(-) diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs index 0de90ea9f217..a5ffe6877fea 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs @@ -3,6 +3,7 @@ using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Extensions; +using Microsoft.Dnx.Runtime; namespace Microsoft.AspNet.NodeServices.Angular { @@ -23,15 +24,16 @@ public class AngularPrerenderTagHelper : TagHelper private IHttpContextAccessor contextAccessor; private INodeServices nodeServices; - public AngularPrerenderTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) + public AngularPrerenderTagHelper(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; - this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; + this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices; // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() // in your startup file, but then again it might be confusing that you don't need to. if (this.nodeServices == null) { - this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http); + var appEnv = (IApplicationEnvironment)serviceProvider.GetService(typeof (IApplicationEnvironment)); + this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http, appEnv.ApplicationBasePath); } } diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.NodeServices.Angular/project.json index 18a0e91273ec..df9e8f854da4 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.NodeServices.Angular/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha4", + "version": "1.0.0-alpha5", "description": "Microsoft.AspNet.NodeServices.Angular Class Library", "authors": [ "Microsoft" @@ -25,8 +25,9 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha4", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha5", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", + "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs b/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs index 7ca084ac3279..b2df3b830b4d 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs +++ b/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs @@ -3,6 +3,7 @@ using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Extensions; +using Microsoft.Dnx.Runtime; namespace Microsoft.AspNet.NodeServices.React { @@ -23,15 +24,16 @@ public class ReactPrerenderTagHelper : TagHelper private IHttpContextAccessor contextAccessor; private INodeServices nodeServices; - public ReactPrerenderTagHelper(IServiceProvider nodeServices, IHttpContextAccessor contextAccessor) + public ReactPrerenderTagHelper(IServiceProvider serviceProvider, IHttpContextAccessor contextAccessor) { this.contextAccessor = contextAccessor; - this.nodeServices = (INodeServices)nodeServices.GetService(typeof (INodeServices)) ?? fallbackNodeServices; + this.nodeServices = (INodeServices)serviceProvider.GetService(typeof (INodeServices)) ?? fallbackNodeServices; // Consider removing the following. Having it means you can get away with not putting app.AddNodeServices() // in your startup file, but then again it might be confusing that you don't need to. if (this.nodeServices == null) { - this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http); + var appEnv = (IApplicationEnvironment)serviceProvider.GetService(typeof(IApplicationEnvironment)); + this.nodeServices = fallbackNodeServices = Configuration.CreateNodeServices(NodeHostingModel.Http, appEnv.ApplicationBasePath); } } diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.NodeServices.React/project.json index c0c6b0399e56..9b765c8565e0 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.NodeServices.React/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha4", + "version": "1.0.0-alpha5", "description": "Microsoft.AspNet.NodeServices.React Class Library", "authors": [ "Microsoft" @@ -25,8 +25,9 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha4", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha5", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", + "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.NodeServices/Configuration.cs b/Microsoft.AspNet.NodeServices/Configuration.cs index 2323812d42ba..7a216630c54f 100644 --- a/Microsoft.AspNet.NodeServices/Configuration.cs +++ b/Microsoft.AspNet.NodeServices/Configuration.cs @@ -1,21 +1,23 @@ +using Microsoft.Dnx.Runtime; using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.NodeServices { public static class Configuration { public static void AddNodeServices(this IServiceCollection serviceCollection, NodeHostingModel hostingModel = NodeHostingModel.Http) { serviceCollection.AddSingleton(typeof(INodeServices), (serviceProvider) => { - return CreateNodeServices(hostingModel); + var appEnv = serviceProvider.GetRequiredService(); + return CreateNodeServices(hostingModel, appEnv.ApplicationBasePath); }); } - public static INodeServices CreateNodeServices(NodeHostingModel hostingModel) + public static INodeServices CreateNodeServices(NodeHostingModel hostingModel, string projectPath) { switch (hostingModel) { case NodeHostingModel.Http: - return new HttpNodeInstance(); + return new HttpNodeInstance(projectPath); case NodeHostingModel.InputOutputStream: - return new InputOutputStreamNodeInstance(); + return new InputOutputStreamNodeInstance(projectPath); default: throw new System.ArgumentException("Unknown hosting model: " + hostingModel.ToString()); } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs index 915f92a87bf9..6c94f2ada2ba 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs @@ -15,8 +15,8 @@ internal class HttpNodeInstance : OutOfProcessNodeInstance { private int _portNumber; - public HttpNodeInstance(int port = 0) - : base(EmbeddedResourceReader.Read(typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), port.ToString()) + public HttpNodeInstance(string projectPath, int port = 0) + : base(EmbeddedResourceReader.Read(typeof(HttpNodeInstance), "/Content/Node/entrypoint-http.js"), projectPath, port.ToString()) { } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs index 0f799351d809..e44d78852d99 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs @@ -24,8 +24,8 @@ internal class InputOutputStreamNodeInstance : OutOfProcessNodeInstance ContractResolver = new CamelCasePropertyNamesContractResolver() }; - public InputOutputStreamNodeInstance() - : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeInstance), "/Content/Node/entrypoint-stream.js")) + public InputOutputStreamNodeInstance(string projectPath) + : base(EmbeddedResourceReader.Read(typeof(InputOutputStreamNodeInstance), "/Content/Node/entrypoint-stream.js"), projectPath) { } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index 5fa5e5379feb..b2876a6b984e 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -12,6 +12,7 @@ public abstract class OutOfProcessNodeInstance : INodeServices { private object _childProcessLauncherLock; private bool disposed; private StringAsTempFile _entryPointScript; + private string _projectPath; private string _commandLineArguments; private Process _nodeProcess; private TaskCompletionSource _nodeProcessIsReadySource; @@ -24,10 +25,11 @@ protected Process NodeProcess { } } - public OutOfProcessNodeInstance(string entryPointScript, string commandLineArguments = null) + public OutOfProcessNodeInstance(string entryPointScript, string projectPath, string commandLineArguments = null) { this._childProcessLauncherLock = new object(); this._entryPointScript = new StringAsTempFile(entryPointScript); + this._projectPath = projectPath; this._commandLineArguments = commandLineArguments ?? string.Empty; } @@ -49,20 +51,20 @@ protected async Task EnsureReady() { lock (this._childProcessLauncherLock) { if (this._nodeProcess == null || this._nodeProcess.HasExited) { var startInfo = new ProcessStartInfo("node") { - Arguments = this._entryPointScript.FileName + " " + this._commandLineArguments, + Arguments = "\"" + this._entryPointScript.FileName + "\" " + this._commandLineArguments, UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, RedirectStandardError = true }; - // Append current directory to NODE_PATH so it can locate node_modules + // Append projectPath to NODE_PATH so it can locate node_modules var existingNodePath = Environment.GetEnvironmentVariable("NODE_PATH") ?? string.Empty; if (existingNodePath != string.Empty) { existingNodePath += ":"; } - var nodePathValue = existingNodePath + Path.Combine(Directory.GetCurrentDirectory(), "node_modules"); + var nodePathValue = existingNodePath + Path.Combine(this._projectPath, "node_modules"); #if DNX451 startInfo.EnvironmentVariables.Add("NODE_PATH", nodePathValue); #else diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index 1c30addaddc1..1d42c81e0d56 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,19 +1,22 @@ { - "version": "1.0.0-alpha4", + "version": "1.0.0-alpha5", "description": "Microsoft.AspNet.NodeServices", - "authors": [ "Microsoft" ], - "tags": [""], + "authors": [ + "Microsoft" + ], + "tags": [ + "" + ], "projectUrl": "", "licenseUrl": "", - "dependencies": { - "System.Net.Http": "4.0.1-beta-23409", - "Newtonsoft.Json": "8.0.1-beta1", - "Microsoft.Framework.DependencyInjection": "1.0.0-beta8" + "System.Net.Http": "4.0.1-beta-23409", + "Newtonsoft.Json": "8.0.1-beta1", + "Microsoft.Framework.DependencyInjection": "1.0.0-beta8", + "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, - "frameworks": { - "dnx451": { }, + "dnx451": {}, "dnxcore50": { "dependencies": { "Microsoft.CSharp": "4.0.1-beta-23217", @@ -28,8 +31,7 @@ } } }, - "resource": [ - "Content/**/*" + "Content/**/*" ] -} +} \ No newline at end of file diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 94df1a6e093b..07f8327639ea 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,7 +19,8 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha4" + "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha5", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha5" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json index d8be5db0c675..b4b260446677 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/ES2015Transpilation/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices": "1.0.0-alpha3" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha5" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index ed9de5ec409f..275ce968ed9e 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha4" + "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha5" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" From 52953c5fe9e3c287c94538dc67bd5e5713241729 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 5 Nov 2015 12:25:29 -0800 Subject: [PATCH 0017/1585] Fix working directory (e.g., for when running under IIS Express) --- .../HostingModels/OutOfProcessNodeInstance.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index b2876a6b984e..e6accf0ae9a6 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -55,7 +55,8 @@ protected async Task EnsureReady() { UseShellExecute = false, RedirectStandardInput = true, RedirectStandardOutput = true, - RedirectStandardError = true + RedirectStandardError = true, + WorkingDirectory = this._projectPath }; // Append projectPath to NODE_PATH so it can locate node_modules From 54aad643c8584e6d42f538667819ec7591c748bb Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 5 Nov 2015 22:05:43 +0000 Subject: [PATCH 0018/1585] Decode Node's JSON response into arbitrary .NET type. Add VS stuff. --- .gitignore | 1 + .../AngularRenderer.cs | 2 +- ...icrosoft.AspNet.NodeServices.Angular.xproj | 19 ++++++ .../Microsoft.AspNet.NodeServices.React.xproj | 19 ++++++ .../ReactRenderer.cs | 2 +- .../Content/Node/entrypoint-http.js | 6 +- .../Content/Node/entrypoint-stream.js | 2 +- .../HostingModels/HttpNodeInstance.cs | 11 +++- .../InputOutputStreamNodeInstance.cs | 5 +- .../HostingModels/OutOfProcessNodeInstance.cs | 10 +-- .../INodeInstance.cs | 4 +- .../Microsoft.AspNet.NodeServices.xproj | 19 ++++++ NodeServices.sln | 64 +++++++++++++++++++ samples/angular/MusicStore/MusicStore.xproj | 19 ++++++ samples/angular/MusicStore/wwwroot/web.config | 8 +-- .../ES2015Transpilation.xproj | 19 ++++++ samples/misc/ES2015Transpilation/Startup.cs | 2 +- .../ES2015Transpilation/wwwroot/web.config | 8 +-- samples/react/ReactGrid/ReactGrid.xproj | 19 ++++++ 19 files changed, 213 insertions(+), 26 deletions(-) create mode 100644 Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj create mode 100644 Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj create mode 100644 Microsoft.AspNet.NodeServices/Microsoft.AspNet.NodeServices.xproj create mode 100644 NodeServices.sln create mode 100644 samples/angular/MusicStore/MusicStore.xproj create mode 100644 samples/misc/ES2015Transpilation/ES2015Transpilation.xproj create mode 100644 samples/react/ReactGrid/ReactGrid.xproj diff --git a/.gitignore b/.gitignore index 4fc10d27b394..0d2dd9c9187e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .vs *.xproj.user project.lock.json +npm-debug.log diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs b/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs index 4ca5868857fe..511a53c8f6df 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs +++ b/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs @@ -13,7 +13,7 @@ static AngularRenderer() { } public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string componentTagName, string requestUrl) { - return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { + return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { moduleName = componentModuleName, exportName = componentExportName, tagName = componentTagName, diff --git a/Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj b/Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj new file mode 100644 index 000000000000..a8b1bbe51bdf --- /dev/null +++ b/Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 421807e6-b62c-417b-b901-46c5dedaa8f1 + Microsoft.AspNet.NodeServices.Angular + ..\artifacts\obj\$(MSBuildProjectName) + ..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj b/Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj new file mode 100644 index 000000000000..2e0a0c829461 --- /dev/null +++ b/Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b04381de-991f-4831-a0b5-fe1bd3ef80c4 + Microsoft.AspNet.NodeServices.React + ..\artifacts\obj\$(MSBuildProjectName) + ..\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs index d48a694f7a82..222d031e30c3 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs +++ b/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs @@ -13,7 +13,7 @@ static ReactRenderer() { } public static async Task RenderToString(INodeServices nodeServices, string componentModuleName, string componentExportName, string requestUrl) { - return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { + return await nodeServices.InvokeExport(nodeScript.FileName, "renderToString", new { moduleName = componentModuleName, exportName = componentExportName, requestUrl = requestUrl diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js index 16d2a7729166..f19c114b365f 100644 --- a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -14,15 +14,15 @@ var server = http.createServer(function(req, res) { if (!func) { throw new Error('The module "' + resolvedPath + '" has no export named "' + bodyJson.exportedFunctionName + '"'); } - + var hasSentResult = false; var callback = function(errorValue, successValue) { if (!hasSentResult) { hasSentResult = true; if (errorValue) { res.status(500).send(errorValue); - } else if (typeof successValue === 'object') { - // Arbitrary object - JSON-serialize it + } else if (typeof successValue !== 'string') { + // Arbitrary object/number/etc - JSON-serialize it res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(successValue)); } else { diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js index 7f5218e9ad58..d2318fa7baf5 100644 --- a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-stream.js @@ -6,7 +6,7 @@ function invocationCallback(errorValue, successValue) { if (errorValue) { throw new Error('InputOutputStreamHost doesn\'t support errors. Got error: ' + errorValue.toString()); } else { - var serializedResult = typeof successValue === 'object' ? JSON.stringify(successValue) : successValue; + var serializedResult = JSON.stringify(successValue); console.log(serializedResult); } } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs index 6c94f2ada2ba..b863ad1fb4d3 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs @@ -20,7 +20,7 @@ public HttpNodeInstance(string projectPath, int port = 0) { } - public override async Task Invoke(NodeInvocationInfo invocationInfo) { + public override async Task Invoke(NodeInvocationInfo invocationInfo) { await this.EnsureReady(); using (var client = new HttpClient()) { @@ -29,7 +29,14 @@ public override async Task Invoke(NodeInvocationInfo invocationInfo) { var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); var response = await client.PostAsync("http://localhost:" + this._portNumber, payload); var responseString = await response.Content.ReadAsStringAsync(); - return responseString; + var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; + if (responseIsJson) { + return JsonConvert.DeserializeObject(responseString); + } else if (typeof(T) != typeof(string)) { + throw new System.ArgumentException("Node module responded with non-JSON string. This cannot be converted to the requested generic type: " + typeof(T).FullName); + } else { + return (T)(object)responseString; + } } } diff --git a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs index e44d78852d99..1408911d484f 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/InputOutputStreamNodeInstance.cs @@ -29,7 +29,7 @@ public InputOutputStreamNodeInstance(string projectPath) { } - public override async Task Invoke(NodeInvocationInfo invocationInfo) { + public override async Task Invoke(NodeInvocationInfo invocationInfo) { await this._invocationSemaphore.WaitAsync(); try { await this.EnsureReady(); @@ -39,7 +39,8 @@ public override async Task Invoke(NodeInvocationInfo invocationInfo) { this._currentInvocationResult = new TaskCompletionSource(); nodeProcess.StandardInput.Write("\ninvoke:"); nodeProcess.StandardInput.WriteLine(payloadJson); // WriteLineAsync isn't supported cross-platform - return await this._currentInvocationResult.Task; + var resultString = await this._currentInvocationResult.Task; + return JsonConvert.DeserializeObject(resultString); } finally { this._invocationSemaphore.Release(); this._currentInvocationResult = null; diff --git a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs index e6accf0ae9a6..0d4ce17374ab 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/OutOfProcessNodeInstance.cs @@ -33,14 +33,14 @@ public OutOfProcessNodeInstance(string entryPointScript, string projectPath, str this._commandLineArguments = commandLineArguments ?? string.Empty; } - public abstract Task Invoke(NodeInvocationInfo invocationInfo); + public abstract Task Invoke(NodeInvocationInfo invocationInfo); - public Task Invoke(string moduleName, params object[] args) { - return this.InvokeExport(moduleName, null, args); + public Task Invoke(string moduleName, params object[] args) { + return this.InvokeExport(moduleName, null, args); } - public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { - return await this.Invoke(new NodeInvocationInfo { + public async Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args) { + return await this.Invoke(new NodeInvocationInfo { ModuleName = moduleName, ExportedFunctionName = exportedFunctionName, Args = args diff --git a/Microsoft.AspNet.NodeServices/INodeInstance.cs b/Microsoft.AspNet.NodeServices/INodeInstance.cs index 9118688f8447..84646756abf9 100644 --- a/Microsoft.AspNet.NodeServices/INodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/INodeInstance.cs @@ -3,8 +3,8 @@ namespace Microsoft.AspNet.NodeServices { public interface INodeServices : IDisposable { - Task Invoke(string moduleName, params object[] args); + Task Invoke(string moduleName, params object[] args); - Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); + Task InvokeExport(string moduleName, string exportedFunctionName, params object[] args); } } diff --git a/Microsoft.AspNet.NodeServices/Microsoft.AspNet.NodeServices.xproj b/Microsoft.AspNet.NodeServices/Microsoft.AspNet.NodeServices.xproj new file mode 100644 index 000000000000..a02a3497d7bf --- /dev/null +++ b/Microsoft.AspNet.NodeServices/Microsoft.AspNet.NodeServices.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + b0fa4175-8b29-4904-9780-28b3c24b0567 + Microsoft.AspNet.NodeServices + ..\NodeServices.sln\artifacts\obj\$(MSBuildProjectName) + ..\NodeServices.sln\artifacts\bin\$(MSBuildProjectName)\ + + + + 2.0 + + + \ No newline at end of file diff --git a/NodeServices.sln b/NodeServices.sln new file mode 100644 index 000000000000..1ded2ba931a5 --- /dev/null +++ b/NodeServices.sln @@ -0,0 +1,64 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{E6E88944-4800-40BA-8AF5-069EA3ADFEB8}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.NodeServices", "Microsoft.AspNet.NodeServices\Microsoft.AspNet.NodeServices.xproj", "{B0FA4175-8B29-4904-9780-28B3C24B0567}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ES2015Transpilation", "samples\misc\ES2015Transpilation\ES2015Transpilation.xproj", "{6D4BCDD6-7951-449B-BE55-CB7F014B7430}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78DAC76C-1092-45AB-BF0D-594B8C7B6569}" + ProjectSection(SolutionItems) = preProject + global.json = global.json + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MusicStore", "samples\angular\MusicStore\MusicStore.xproj", "{1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReactGrid", "samples\react\ReactGrid\ReactGrid.xproj", "{ABF90A5B-F4E0-438C-A6E4-9549FB43690B}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.NodeServices.Angular", "Microsoft.AspNet.NodeServices.Angular\Microsoft.AspNet.NodeServices.Angular.xproj", "{421807E6-B62C-417B-B901-46C5DEDAA8F1}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.NodeServices.React", "Microsoft.AspNet.NodeServices.React\Microsoft.AspNet.NodeServices.React.xproj", "{B04381DE-991F-4831-A0B5-FE1BD3EF80C4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B0FA4175-8B29-4904-9780-28B3C24B0567}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0FA4175-8B29-4904-9780-28B3C24B0567}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0FA4175-8B29-4904-9780-28B3C24B0567}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0FA4175-8B29-4904-9780-28B3C24B0567}.Release|Any CPU.Build.0 = Release|Any CPU + {6D4BCDD6-7951-449B-BE55-CB7F014B7430}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6D4BCDD6-7951-449B-BE55-CB7F014B7430}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D4BCDD6-7951-449B-BE55-CB7F014B7430}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6D4BCDD6-7951-449B-BE55-CB7F014B7430}.Release|Any CPU.Build.0 = Release|Any CPU + {1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B}.Release|Any CPU.Build.0 = Release|Any CPU + {ABF90A5B-F4E0-438C-A6E4-9549FB43690B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABF90A5B-F4E0-438C-A6E4-9549FB43690B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABF90A5B-F4E0-438C-A6E4-9549FB43690B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABF90A5B-F4E0-438C-A6E4-9549FB43690B}.Release|Any CPU.Build.0 = Release|Any CPU + {421807E6-B62C-417B-B901-46C5DEDAA8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {421807E6-B62C-417B-B901-46C5DEDAA8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {421807E6-B62C-417B-B901-46C5DEDAA8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {421807E6-B62C-417B-B901-46C5DEDAA8F1}.Release|Any CPU.Build.0 = Release|Any CPU + {B04381DE-991F-4831-A0B5-FE1BD3EF80C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B04381DE-991F-4831-A0B5-FE1BD3EF80C4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B04381DE-991F-4831-A0B5-FE1BD3EF80C4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B04381DE-991F-4831-A0B5-FE1BD3EF80C4}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {6D4BCDD6-7951-449B-BE55-CB7F014B7430} = {E6E88944-4800-40BA-8AF5-069EA3ADFEB8} + {1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B} = {E6E88944-4800-40BA-8AF5-069EA3ADFEB8} + {ABF90A5B-F4E0-438C-A6E4-9549FB43690B} = {E6E88944-4800-40BA-8AF5-069EA3ADFEB8} + EndGlobalSection +EndGlobal diff --git a/samples/angular/MusicStore/MusicStore.xproj b/samples/angular/MusicStore/MusicStore.xproj new file mode 100644 index 000000000000..b4d622ba5c9c --- /dev/null +++ b/samples/angular/MusicStore/MusicStore.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1a74148f-9dc0-435d-b5ac-7d1b0d3d5e0b + MusicStore + ..\artifacts\obj\$(MSBuildProjectName) + ..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 5068 + + + \ No newline at end of file diff --git a/samples/angular/MusicStore/wwwroot/web.config b/samples/angular/MusicStore/wwwroot/web.config index db6e6f4582dc..f51dd9a18306 100644 --- a/samples/angular/MusicStore/wwwroot/web.config +++ b/samples/angular/MusicStore/wwwroot/web.config @@ -1,9 +1,9 @@ - + - + - + - + \ No newline at end of file diff --git a/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj b/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj new file mode 100644 index 000000000000..7b9d1f7b0a8d --- /dev/null +++ b/samples/misc/ES2015Transpilation/ES2015Transpilation.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 6d4bcdd6-7951-449b-be55-cb7f014b7430 + ES2015Transpilation + ..\..\..\NodeServices.sln\artifacts\obj\$(MSBuildProjectName) + ..\..\..\NodeServices.sln\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 2018 + + + \ No newline at end of file diff --git a/samples/misc/ES2015Transpilation/Startup.cs b/samples/misc/ES2015Transpilation/Startup.cs index a95bd1abd429..80e7e5f4617e 100755 --- a/samples/misc/ES2015Transpilation/Startup.cs +++ b/samples/misc/ES2015Transpilation/Startup.cs @@ -63,7 +63,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF if (requestPath.StartsWith("/js/") && requestPath.EndsWith(".js")) { var fileInfo = env.WebRootFileProvider.GetFileInfo(requestPath); if (fileInfo.Exists) { - var transpiled = await nodeServices.Invoke("transpilation.js", fileInfo.PhysicalPath, requestPath); + var transpiled = await nodeServices.Invoke("transpilation.js", fileInfo.PhysicalPath, requestPath); await context.Response.WriteAsync(transpiled); return; } diff --git a/samples/misc/ES2015Transpilation/wwwroot/web.config b/samples/misc/ES2015Transpilation/wwwroot/web.config index db6e6f4582dc..f51dd9a18306 100644 --- a/samples/misc/ES2015Transpilation/wwwroot/web.config +++ b/samples/misc/ES2015Transpilation/wwwroot/web.config @@ -1,9 +1,9 @@ - + - + - + - + \ No newline at end of file diff --git a/samples/react/ReactGrid/ReactGrid.xproj b/samples/react/ReactGrid/ReactGrid.xproj new file mode 100644 index 000000000000..d2d598cc1bca --- /dev/null +++ b/samples/react/ReactGrid/ReactGrid.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + abf90a5b-f4e0-438c-a6e4-9549fb43690b + ReactGrid + ..\..\..\artifacts\obj\$(MSBuildProjectName) + ..\..\..\artifacts\bin\$(MSBuildProjectName)\ + + + 2.0 + 2311 + + + \ No newline at end of file From 717fb37034f949a3bb3624d73d2685f2be267c34 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 5 Nov 2015 14:23:11 -0800 Subject: [PATCH 0019/1585] Rename packages. Bump versions to -alpha6 --- .../.gitignore | 0 .../AngularPrerenderTagHelper.cs | 3 ++- .../AngularRenderer.cs | 3 ++- .../Content/Node/angular-rendering.js | 0 .../Microsoft.AspNet.AngularServices.xproj | 2 +- .../project.json | 8 ++++---- Microsoft.AspNet.NodeServices/project.json | 2 +- .../.gitignore | 0 .../Content/Node/react-rendering.js | 0 .../Microsoft.AspNet.ReactServices.xproj | 2 +- .../ReactPrerenderTagHelper.cs | 4 ++-- .../ReactRenderer.cs | 3 ++- .../project.json | 8 ++++---- NodeServices.sln | 4 ++-- samples/angular/MusicStore/Views/_ViewImports.cshtml | 2 +- samples/angular/MusicStore/project.json | 3 +-- samples/misc/ES2015Transpilation/project.json | 2 +- samples/react/ReactGrid/Views/_ViewImports.cshtml | 2 +- samples/react/ReactGrid/project.json | 2 +- 19 files changed, 26 insertions(+), 24 deletions(-) rename {Microsoft.AspNet.NodeServices.Angular => Microsoft.AspNet.AngularServices}/.gitignore (100%) rename {Microsoft.AspNet.NodeServices.Angular => Microsoft.AspNet.AngularServices}/AngularPrerenderTagHelper.cs (96%) rename {Microsoft.AspNet.NodeServices.Angular => Microsoft.AspNet.AngularServices}/AngularRenderer.cs (92%) rename {Microsoft.AspNet.NodeServices.Angular => Microsoft.AspNet.AngularServices}/Content/Node/angular-rendering.js (100%) rename Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj => Microsoft.AspNet.AngularServices/Microsoft.AspNet.AngularServices.xproj (93%) rename {Microsoft.AspNet.NodeServices.React => Microsoft.AspNet.AngularServices}/project.json (74%) rename {Microsoft.AspNet.NodeServices.React => Microsoft.AspNet.ReactServices}/.gitignore (100%) rename {Microsoft.AspNet.NodeServices.React => Microsoft.AspNet.ReactServices}/Content/Node/react-rendering.js (100%) rename Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj => Microsoft.AspNet.ReactServices/Microsoft.AspNet.ReactServices.xproj (93%) rename {Microsoft.AspNet.NodeServices.React => Microsoft.AspNet.ReactServices}/ReactPrerenderTagHelper.cs (96%) rename {Microsoft.AspNet.NodeServices.React => Microsoft.AspNet.ReactServices}/ReactRenderer.cs (91%) rename {Microsoft.AspNet.NodeServices.Angular => Microsoft.AspNet.ReactServices}/project.json (73%) diff --git a/Microsoft.AspNet.NodeServices.Angular/.gitignore b/Microsoft.AspNet.AngularServices/.gitignore similarity index 100% rename from Microsoft.AspNet.NodeServices.Angular/.gitignore rename to Microsoft.AspNet.AngularServices/.gitignore diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs similarity index 96% rename from Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs rename to Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs index a5ffe6877fea..bacb3dc4de65 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs @@ -3,9 +3,10 @@ using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Extensions; +using Microsoft.AspNet.NodeServices; using Microsoft.Dnx.Runtime; -namespace Microsoft.AspNet.NodeServices.Angular +namespace Microsoft.AspNet.AngularServices { [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] public class AngularPrerenderTagHelper : TagHelper diff --git a/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs b/Microsoft.AspNet.AngularServices/AngularRenderer.cs similarity index 92% rename from Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs rename to Microsoft.AspNet.AngularServices/AngularRenderer.cs index 511a53c8f6df..5c52cb6b0eed 100644 --- a/Microsoft.AspNet.NodeServices.Angular/AngularRenderer.cs +++ b/Microsoft.AspNet.AngularServices/AngularRenderer.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; +using Microsoft.AspNet.NodeServices; -namespace Microsoft.AspNet.NodeServices.Angular +namespace Microsoft.AspNet.AngularServices { public static class AngularRenderer { diff --git a/Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js b/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js similarity index 100% rename from Microsoft.AspNet.NodeServices.Angular/Content/Node/angular-rendering.js rename to Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js diff --git a/Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj b/Microsoft.AspNet.AngularServices/Microsoft.AspNet.AngularServices.xproj similarity index 93% rename from Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj rename to Microsoft.AspNet.AngularServices/Microsoft.AspNet.AngularServices.xproj index a8b1bbe51bdf..984caa4fdd2b 100644 --- a/Microsoft.AspNet.NodeServices.Angular/Microsoft.AspNet.NodeServices.Angular.xproj +++ b/Microsoft.AspNet.AngularServices/Microsoft.AspNet.AngularServices.xproj @@ -7,7 +7,7 @@ 421807e6-b62c-417b-b901-46c5dedaa8f1 - Microsoft.AspNet.NodeServices.Angular + Microsoft.AspNet.AngularServices ..\artifacts\obj\$(MSBuildProjectName) ..\artifacts\bin\$(MSBuildProjectName)\ diff --git a/Microsoft.AspNet.NodeServices.React/project.json b/Microsoft.AspNet.AngularServices/project.json similarity index 74% rename from Microsoft.AspNet.NodeServices.React/project.json rename to Microsoft.AspNet.AngularServices/project.json index 9b765c8565e0..f3bf8a2f02ff 100644 --- a/Microsoft.AspNet.NodeServices.React/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -1,6 +1,6 @@ { - "version": "1.0.0-alpha5", - "description": "Microsoft.AspNet.NodeServices.React Class Library", + "version": "1.0.0-alpha6", + "description": "Microsoft.AspNet.AngularServices Class Library", "authors": [ "Microsoft" ], @@ -10,7 +10,7 @@ "projectUrl": "", "licenseUrl": "", "tooling": { - "defaultNamespace": "Microsoft.AspNet.NodeServices.React" + "defaultNamespace": "Microsoft.AspNet.AngularServices" }, "frameworks": { "dnx451": {}, @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha5", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha6", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index 1d42c81e0d56..7825b83f52b4 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha5", + "version": "1.0.0-alpha6", "description": "Microsoft.AspNet.NodeServices", "authors": [ "Microsoft" diff --git a/Microsoft.AspNet.NodeServices.React/.gitignore b/Microsoft.AspNet.ReactServices/.gitignore similarity index 100% rename from Microsoft.AspNet.NodeServices.React/.gitignore rename to Microsoft.AspNet.ReactServices/.gitignore diff --git a/Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js b/Microsoft.AspNet.ReactServices/Content/Node/react-rendering.js similarity index 100% rename from Microsoft.AspNet.NodeServices.React/Content/Node/react-rendering.js rename to Microsoft.AspNet.ReactServices/Content/Node/react-rendering.js diff --git a/Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj b/Microsoft.AspNet.ReactServices/Microsoft.AspNet.ReactServices.xproj similarity index 93% rename from Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj rename to Microsoft.AspNet.ReactServices/Microsoft.AspNet.ReactServices.xproj index 2e0a0c829461..6da829cdb88b 100644 --- a/Microsoft.AspNet.NodeServices.React/Microsoft.AspNet.NodeServices.React.xproj +++ b/Microsoft.AspNet.ReactServices/Microsoft.AspNet.ReactServices.xproj @@ -7,7 +7,7 @@ b04381de-991f-4831-a0b5-fe1bd3ef80c4 - Microsoft.AspNet.NodeServices.React + Microsoft.AspNet.ReactServices ..\artifacts\obj\$(MSBuildProjectName) ..\artifacts\bin\$(MSBuildProjectName)\ diff --git a/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs b/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs similarity index 96% rename from Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs rename to Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs index b2df3b830b4d..241940349b35 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactPrerenderTagHelper.cs +++ b/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs @@ -2,10 +2,10 @@ using System.Threading.Tasks; using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Extensions; +using Microsoft.AspNet.NodeServices; using Microsoft.Dnx.Runtime; -namespace Microsoft.AspNet.NodeServices.React +namespace Microsoft.AspNet.ReactServices { [HtmlTargetElement(Attributes = PrerenderModuleAttributeName)] public class ReactPrerenderTagHelper : TagHelper diff --git a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs b/Microsoft.AspNet.ReactServices/ReactRenderer.cs similarity index 91% rename from Microsoft.AspNet.NodeServices.React/ReactRenderer.cs rename to Microsoft.AspNet.ReactServices/ReactRenderer.cs index 222d031e30c3..e97f39ecd0ad 100644 --- a/Microsoft.AspNet.NodeServices.React/ReactRenderer.cs +++ b/Microsoft.AspNet.ReactServices/ReactRenderer.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; +using Microsoft.AspNet.NodeServices; -namespace Microsoft.AspNet.NodeServices.React +namespace Microsoft.AspNet.ReactServices { public static class ReactRenderer { diff --git a/Microsoft.AspNet.NodeServices.Angular/project.json b/Microsoft.AspNet.ReactServices/project.json similarity index 73% rename from Microsoft.AspNet.NodeServices.Angular/project.json rename to Microsoft.AspNet.ReactServices/project.json index df9e8f854da4..d102fce37e34 100644 --- a/Microsoft.AspNet.NodeServices.Angular/project.json +++ b/Microsoft.AspNet.ReactServices/project.json @@ -1,6 +1,6 @@ { - "version": "1.0.0-alpha5", - "description": "Microsoft.AspNet.NodeServices.Angular Class Library", + "version": "1.0.0-alpha6", + "description": "Microsoft.AspNet.ReactServices Class Library", "authors": [ "Microsoft" ], @@ -10,7 +10,7 @@ "projectUrl": "", "licenseUrl": "", "tooling": { - "defaultNamespace": "Microsoft.AspNet.NodeServices.Angular" + "defaultNamespace": "Microsoft.AspNet.ReactServices" }, "frameworks": { "dnx451": {}, @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha5", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha6", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, diff --git a/NodeServices.sln b/NodeServices.sln index 1ded2ba931a5..b79e3b1379cd 100644 --- a/NodeServices.sln +++ b/NodeServices.sln @@ -18,9 +18,9 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MusicStore", "samples\angul EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ReactGrid", "samples\react\ReactGrid\ReactGrid.xproj", "{ABF90A5B-F4E0-438C-A6E4-9549FB43690B}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.NodeServices.Angular", "Microsoft.AspNet.NodeServices.Angular\Microsoft.AspNet.NodeServices.Angular.xproj", "{421807E6-B62C-417B-B901-46C5DEDAA8F1}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.AngularServices", "Microsoft.AspNet.AngularServices\Microsoft.AspNet.AngularServices.xproj", "{421807E6-B62C-417B-B901-46C5DEDAA8F1}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.NodeServices.React", "Microsoft.AspNet.NodeServices.React\Microsoft.AspNet.NodeServices.React.xproj", "{B04381DE-991F-4831-A0B5-FE1BD3EF80C4}" +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.ReactServices", "Microsoft.AspNet.ReactServices\Microsoft.AspNet.ReactServices.xproj", "{B04381DE-991F-4831-A0B5-FE1BD3EF80C4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/samples/angular/MusicStore/Views/_ViewImports.cshtml b/samples/angular/MusicStore/Views/_ViewImports.cshtml index 808b5ca460b5..24b10f41a501 100755 --- a/samples/angular/MusicStore/Views/_ViewImports.cshtml +++ b/samples/angular/MusicStore/Views/_ViewImports.cshtml @@ -1,3 +1,3 @@ @using MusicStore @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" -@addTagHelper "*, Microsoft.AspNet.NodeServices.Angular" +@addTagHelper "*, Microsoft.AspNet.AngularServices" diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 07f8327639ea..7c9b638fb756 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,8 +19,7 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.NodeServices.Angular": "1.0.0-alpha5", - "Microsoft.AspNet.NodeServices": "1.0.0-alpha5" + "Microsoft.AspNet.AngularServices": "1.0.0-alpha6" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json index b4b260446677..55d28068b245 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/ES2015Transpilation/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices": "1.0.0-alpha5" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha6" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/Views/_ViewImports.cshtml b/samples/react/ReactGrid/Views/_ViewImports.cshtml index 8237608fdcfa..5fb314717b65 100755 --- a/samples/react/ReactGrid/Views/_ViewImports.cshtml +++ b/samples/react/ReactGrid/Views/_ViewImports.cshtml @@ -1,3 +1,3 @@ @using ReactExample @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" -@addTagHelper "*, Microsoft.AspNet.NodeServices.React" +@addTagHelper "*, Microsoft.AspNet.ReactServices" diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index 275ce968ed9e..d2198553b766 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices.React": "1.0.0-alpha5" + "Microsoft.AspNet.ReactServices": "1.0.0-alpha6" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" From bf910eb76ece1255569981c532960bb5efb51dbb Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 17 Nov 2015 11:16:52 +0000 Subject: [PATCH 0020/1585] Bump all versions to -alpha7 --- Microsoft.AspNet.AngularServices/project.json | 4 ++-- Microsoft.AspNet.NodeServices/project.json | 2 +- Microsoft.AspNet.ReactServices/project.json | 4 ++-- samples/angular/MusicStore/project.json | 2 +- samples/misc/ES2015Transpilation/project.json | 2 +- samples/react/ReactGrid/project.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index f3bf8a2f02ff..36060d1e6895 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha6", + "version": "1.0.0-alpha7", "description": "Microsoft.AspNet.AngularServices Class Library", "authors": [ "Microsoft" @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha6", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index 7825b83f52b4..b8da71ae0537 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha6", + "version": "1.0.0-alpha7", "description": "Microsoft.AspNet.NodeServices", "authors": [ "Microsoft" diff --git a/Microsoft.AspNet.ReactServices/project.json b/Microsoft.AspNet.ReactServices/project.json index d102fce37e34..37929e49efda 100644 --- a/Microsoft.AspNet.ReactServices/project.json +++ b/Microsoft.AspNet.ReactServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha6", + "version": "1.0.0-alpha7", "description": "Microsoft.AspNet.ReactServices Class Library", "authors": [ "Microsoft" @@ -25,7 +25,7 @@ } }, "dependencies": { - "Microsoft.AspNet.NodeServices": "1.0.0-alpha6", + "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" }, diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index 7c9b638fb756..db764ce209bd 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -19,7 +19,7 @@ "EntityFramework.SQLite": "7.0.0-beta8", "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.AngularServices": "1.0.0-alpha6" + "Microsoft.AspNet.AngularServices": "1.0.0-alpha7" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json index 55d28068b245..11769790b981 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/ES2015Transpilation/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.NodeServices": "1.0.0-alpha6" + "Microsoft.AspNet.NodeServices": "1.0.0-alpha7" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index d2198553b766..167a3a0779d8 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -16,7 +16,7 @@ "Microsoft.Framework.Logging": "1.0.0-beta8", "Microsoft.Framework.Logging.Console": "1.0.0-beta8", "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "Microsoft.AspNet.ReactServices": "1.0.0-alpha6" + "Microsoft.AspNet.ReactServices": "1.0.0-alpha7" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" From 1d817e072c1f482fca7adbab730cfbfa51ebec92 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 17 Nov 2015 11:29:59 +0000 Subject: [PATCH 0021/1585] Add global.json so packages can be consumed as source --- global.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 global.json diff --git a/global.json b/global.json new file mode 100644 index 000000000000..6a7d89b0d292 --- /dev/null +++ b/global.json @@ -0,0 +1,3 @@ +{ + "projects": ["."] +} From b1a7ac1f5069069446b6731b60199a0cba1f731b Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 17 Nov 2015 13:08:56 +0000 Subject: [PATCH 0022/1585] Update README.md --- README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 96af369095c2..2a5eb3843eb2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,66 @@ NodeServices ======== -This repo hosts the Microsoft.AspNet.NodeServices project. +This repo hosts sources for the `Microsoft.AspNet.AngularServices` and `Microsoft.AspNet.ReactServices` packages, along with samples and the underlying `Microsoft.AspNet.NodeServices project`. This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. + +## Trying the samples + +To get started, + +1. Ensure you have [installed the latest stable version of ASP.NET 5](https://www.asp.net/vnext). Instructions are available for [Windows](http://docs.asp.net/en/latest/getting-started/installing-on-windows.html), [Mac](http://docs.asp.net/en/latest/getting-started/installing-on-mac.html), and [Linux](http://docs.asp.net/en/latest/getting-started/installing-on-linux.html). +2. Ensure you have [installed a recent version of Node.js](https://nodejs.org/en/). To check this works, open a console prompt, and type `node -v`. It should print a version number. +3. Ensure you have installed `gulp` globally. You can check if it's there by running `gulp -v`. If you need to install it: + + ``` + npm install -g gulp + ``` + +3. Clone this repository: + + ``` + git clone https://github.com/aspnet/NodeServices.git + ``` + +**Using Visual Studio on Windows** + +1. Open the solution file, `NodeServices.sln`, in Visual Studio. Wait for it to finish fetching and installing dependencies. + +**Using dnx on Windows/Mac/Linux** + +1. Ensure you are using a suitable .NET runtime. Currently, this project is tested with version `1.0.0-beta8` on `coreclr`: + + ``` + dnvm use 1.0.0-beta8 -r coreclr + ``` + +2. In the solution root directory (`NodeServices` - i.e., the directory that contains `NodeServices.sln`), restore the .NET dependencies: + + + ``` + cd NodeServices + dnu restore + ``` + +3. Change directory to whichever sample you want to run, then restore the Node dependencies. For example: + + ``` + cd samples/angular/MusicStore/ + npm install + ``` + +4. Build the project + + ``` + gulp + ``` + +5. Run the project (and wait until it displays the message `Application started`) + + ``` + dnx web + ``` + +6. Browse to [`http://localhost:5000/`](http://localhost:5000/) + From 3c97a50a792d91c457875d837600b5ca967b1a53 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 17 Nov 2015 13:32:13 +0000 Subject: [PATCH 0023/1585] Update README.md --- README.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a5eb3843eb2..2c290a499c59 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,29 @@ NodeServices ======== +This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. + +## What is this? + This repo hosts sources for the `Microsoft.AspNet.AngularServices` and `Microsoft.AspNet.ReactServices` packages, along with samples and the underlying `Microsoft.AspNet.NodeServices project`. -This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. + * `Microsoft.AspNet.AngularServices` provides facilities for developers building Angular 2 applications on ASP.NET. + * Most notably, this includes **server-side prerendering**. You can build a "universal" (sometimes called "isomorphic") single-page application that renders its initial HTML on the server, and then continues execution on the client. This massively improves application delivery and startup time (often reducing from 5-10+ seconds to 100ms or so, especially on high-latency networks or low-CPU-speed clients), enables search engine crawlers to explore your SPA, and ensures that users don't wait for any 'loading' UI when they first hit your application. A sample is included in this repo. + * We are also working with the Angular team to add support for other client+server features such as cache priming, so that the client-side SPA code does not need to wait for an initial set of ajax requests to complete - the necessary data can be bundled with the initial page. + * Another possible future feature would be helpers to emit a JSON representation of C# class model metadata, so some validation rules can transparently apply both on the server and the client. + + * `Microsoft.AspNet.ReactServices` provides similar facilities for React applications on ASP.NET. + * This includes **server-side prerendering** to support "universal" (sometimes called "isomorphic") single-page applications as described above. A sample is included in this repo. + * We are open to adding other client+server features that will make React developers more productive on ASP.NET. Please let us know if you have specific feature proposals. + + * *Your favourite JavaScript framework goes here*. Although we have finite resources and are currently focused on adding Angular 2 and React support, the architecture here is designed so that you can build your own server-side support for other client-side libraries and frameworks. + * The underlying `Microsoft.AspNet.NodeServices` package is a general-purpose way for ASP.NET applications (or .NET applications more generally) to interoperate with code running inside Node.js. That's how `AngularServices`/`ReactServices` server-side rendering works - those packages transparently spin up Node.js instances that can perform the server-side rendering. Any code that runs inside Node can efficiently be invoked from .NET via this package, which takes care of starting and stopping Node instances and manages the communication between .NET and Node. + +## Using AngularServices/ReactServices in your own projects + +Currently it's a little early for production use in real projects, because for example AngularServices uses [angular-universal](https://github.com/angular/universal), which is still in prerelease. Some important features are either not implemented or are not yet stable. The React support is more solid. + +If you're a keen early-adopter type, you can infer usage from the samples. Let us know how you get on :) ## Trying the samples From 72f7bdba619a059c36119b0576824bc6ad902f2b Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 17 Nov 2015 13:37:20 +0000 Subject: [PATCH 0024/1585] Update README.md --- README.md | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2c290a499c59..f1580b9bedc1 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,32 @@ This project is part of ASP.NET 5. You can find samples, documentation and getti This repo hosts sources for the `Microsoft.AspNet.AngularServices` and `Microsoft.AspNet.ReactServices` packages, along with samples and the underlying `Microsoft.AspNet.NodeServices project`. - * `Microsoft.AspNet.AngularServices` provides facilities for developers building Angular 2 applications on ASP.NET. - * Most notably, this includes **server-side prerendering**. You can build a "universal" (sometimes called "isomorphic") single-page application that renders its initial HTML on the server, and then continues execution on the client. This massively improves application delivery and startup time (often reducing from 5-10+ seconds to 100ms or so, especially on high-latency networks or low-CPU-speed clients), enables search engine crawlers to explore your SPA, and ensures that users don't wait for any 'loading' UI when they first hit your application. A sample is included in this repo. - * We are also working with the Angular team to add support for other client+server features such as cache priming, so that the client-side SPA code does not need to wait for an initial set of ajax requests to complete - the necessary data can be bundled with the initial page. - * Another possible future feature would be helpers to emit a JSON representation of C# class model metadata, so some validation rules can transparently apply both on the server and the client. +#### `Microsoft.AspNet.AngularServices` - * `Microsoft.AspNet.ReactServices` provides similar facilities for React applications on ASP.NET. - * This includes **server-side prerendering** to support "universal" (sometimes called "isomorphic") single-page applications as described above. A sample is included in this repo. - * We are open to adding other client+server features that will make React developers more productive on ASP.NET. Please let us know if you have specific feature proposals. +This package provides facilities for developers building Angular 2 applications on ASP.NET. - * *Your favourite JavaScript framework goes here*. Although we have finite resources and are currently focused on adding Angular 2 and React support, the architecture here is designed so that you can build your own server-side support for other client-side libraries and frameworks. - * The underlying `Microsoft.AspNet.NodeServices` package is a general-purpose way for ASP.NET applications (or .NET applications more generally) to interoperate with code running inside Node.js. That's how `AngularServices`/`ReactServices` server-side rendering works - those packages transparently spin up Node.js instances that can perform the server-side rendering. Any code that runs inside Node can efficiently be invoked from .NET via this package, which takes care of starting and stopping Node instances and manages the communication between .NET and Node. +Most notably, this includes **server-side prerendering**. You can build a "universal" (sometimes called "isomorphic") single-page application that renders its initial HTML on the server, and then continues execution on the client. Benefits: + * Massively improves application delivery and startup time (often reducing from 5-10+ seconds to 100ms or so, especially on high-latency networks or low-CPU-speed clients) + * Enables search engine crawlers to explore your SPA + * Ensures that users don't wait for any 'loading' UI when they first hit your application. + +A sample is included in this repo. + +We are also working with the Angular team to add support for other client+server features such as cache priming, so that the client-side SPA code does not need to wait for an initial set of ajax requests to complete - the necessary data can be bundled with the initial page. Another possible future feature would be helpers to emit a JSON representation of C# class model metadata, so some validation rules can transparently apply both on the server and the client. + +#### `Microsoft.AspNet.ReactServices` + +This package provides similar facilities for React applications on ASP.NET. + +This includes **server-side prerendering** to support "universal" (sometimes called "isomorphic") single-page applications as described above. A sample is included in this repo. + +We are open to adding other client+server features that will make React developers more productive on ASP.NET. Please let us know if you have specific feature proposals. + +#### *Your favourite JavaScript framework goes here* + +Although we have finite resources and are currently focused on adding Angular 2 and React support, the architecture here is designed so that you can build your own server-side support for other client-side libraries and frameworks. + +The underlying `Microsoft.AspNet.NodeServices` package is a general-purpose way for ASP.NET applications (or .NET applications more generally) to interoperate with code running inside Node.js. That's how `AngularServices`/`ReactServices` server-side rendering works - those packages transparently spin up Node.js instances that can perform the server-side rendering. Any code that runs inside Node can efficiently be invoked from .NET via this package, which takes care of starting and stopping Node instances and manages the communication between .NET and Node. ## Using AngularServices/ReactServices in your own projects From efe51c0c95b047565a5823e02c5eeb21b54eeb83 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 17 Nov 2015 13:56:34 +0000 Subject: [PATCH 0025/1585] Update README.md --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f1580b9bedc1..0974e4951b88 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,10 @@ To get started, **Using Visual Studio on Windows** -1. Open the solution file, `NodeServices.sln`, in Visual Studio. Wait for it to finish fetching and installing dependencies. +1. Open the solution file, `NodeServices.sln`, in Visual Studio. +2. Wait for it to finish fetching and installing dependencies. +3. If you get the error `'reactivex/rxjs' is not in the npm registry`, then your Visual Studio installation's version of the NPM tool is out of date. You will need to restore NPM dependencies manually from a command prompt (e.g., `cd samples\angular\MusicStore` then `npm install`). +4. Select a sample and run it. For example, right-click on the `MusicStore` project in Solution Explorer and choose `Set as startup project`. Then press `Ctrl+F5` to launch it. **Using dnx on Windows/Mac/Linux** From 83c9f2136d1c9125f4841afdaca13c328183f965 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 24 Nov 2015 12:06:24 +0000 Subject: [PATCH 0026/1585] Update everything to use ASP.NET 5 RC1 --- .../AngularPrerenderTagHelper.cs | 6 ++-- Microsoft.AspNet.AngularServices/project.json | 3 +- .../Configuration.cs | 4 +-- Microsoft.AspNet.NodeServices/project.json | 25 ++++++++--------- .../ReactPrerenderTagHelper.cs | 6 ++-- Microsoft.AspNet.ReactServices/project.json | 3 +- global.json | 5 +++- .../Apis/Models/MusicStoreContext.cs | 7 +---- .../MusicStore/Apis/Models/SampleData.cs | 7 ++--- samples/angular/MusicStore/Startup.cs | 17 ++++------- samples/angular/MusicStore/project.json | 28 +++++++++---------- samples/misc/ES2015Transpilation/Startup.cs | 10 +++---- samples/misc/ES2015Transpilation/project.json | 20 ++++++------- samples/react/ReactGrid/Startup.cs | 11 ++++---- samples/react/ReactGrid/project.json | 20 ++++++------- 15 files changed, 76 insertions(+), 96 deletions(-) diff --git a/Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs b/Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs index bacb3dc4de65..b5771cccd640 100644 --- a/Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs +++ b/Microsoft.AspNet.AngularServices/AngularPrerenderTagHelper.cs @@ -1,10 +1,10 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Extensions; using Microsoft.AspNet.NodeServices; -using Microsoft.Dnx.Runtime; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Extensions.PlatformAbstractions; namespace Microsoft.AspNet.AngularServices { @@ -48,7 +48,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu requestUrl: UriHelper.GetEncodedUrl(this.contextAccessor.HttpContext.Request) ); output.SuppressOutput(); - output.PostElement.AppendEncoded(result); + output.PostElement.AppendHtml(result); } } } diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index 36060d1e6895..41c8f1aa07e7 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -26,8 +26,7 @@ }, "dependencies": { "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", - "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.NodeServices/Configuration.cs b/Microsoft.AspNet.NodeServices/Configuration.cs index 7a216630c54f..043cc2daef80 100644 --- a/Microsoft.AspNet.NodeServices/Configuration.cs +++ b/Microsoft.AspNet.NodeServices/Configuration.cs @@ -1,5 +1,5 @@ -using Microsoft.Dnx.Runtime; -using Microsoft.Framework.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.PlatformAbstractions; namespace Microsoft.AspNet.NodeServices { public static class Configuration { diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index b8da71ae0537..f27816164c3f 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -10,24 +10,23 @@ "projectUrl": "", "licenseUrl": "", "dependencies": { - "System.Net.Http": "4.0.1-beta-23409", - "Newtonsoft.Json": "8.0.1-beta1", - "Microsoft.Framework.DependencyInjection": "1.0.0-beta8", - "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" + "System.Net.Http": "4.0.1-beta-*", + "Newtonsoft.Json": "8.0.1-beta3", + "Microsoft.Extensions.DependencyInjection.Abstractions": "1.0.0-rc1-final", + "Microsoft.Extensions.PlatformAbstractions": "1.0.0-rc1-final" }, "frameworks": { "dnx451": {}, "dnxcore50": { "dependencies": { - "Microsoft.CSharp": "4.0.1-beta-23217", - "System.Collections": "4.0.11-beta-23217", - "System.Linq": "4.0.1-beta-23217", - "System.Runtime": "4.0.21-beta-23217", - "System.Threading": "4.0.11-beta-23217", - "System.Text.RegularExpressions": "4.0.11-beta-23409", - "System.Diagnostics.Process": "4.1.0-beta-23409", - "System.IO.FileSystem": "4.0.1-beta-23409", - "System.Console": "4.0.0-beta-23409" + "Microsoft.CSharp": "4.0.1-beta-*", + "System.Collections": "4.0.11-beta-*", + "System.Linq": "4.0.1-beta-*", + "System.Threading": "4.0.11-beta-*", + "System.Text.RegularExpressions": "4.0.11-beta-*", + "System.Diagnostics.Process": "4.1.0-beta-*", + "System.IO.FileSystem": "4.0.1-beta-*", + "System.Console": "4.0.0-beta-*" } } }, diff --git a/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs b/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs index 241940349b35..6d2b761bb33a 100644 --- a/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs +++ b/Microsoft.AspNet.ReactServices/ReactPrerenderTagHelper.cs @@ -1,9 +1,9 @@ using System; using System.Threading.Tasks; -using Microsoft.AspNet.Razor.Runtime.TagHelpers; using Microsoft.AspNet.Http; using Microsoft.AspNet.NodeServices; -using Microsoft.Dnx.Runtime; +using Microsoft.AspNet.Razor.TagHelpers; +using Microsoft.Extensions.PlatformAbstractions; namespace Microsoft.AspNet.ReactServices { @@ -45,7 +45,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu componentModuleName: this.ModuleName, componentExportName: this.ExportName, requestUrl: request.Path + request.QueryString.Value); - output.Content.SetContentEncoded(result); + output.Content.SetHtmlContent(result); } } } diff --git a/Microsoft.AspNet.ReactServices/project.json b/Microsoft.AspNet.ReactServices/project.json index 37929e49efda..c6904f25b994 100644 --- a/Microsoft.AspNet.ReactServices/project.json +++ b/Microsoft.AspNet.ReactServices/project.json @@ -26,8 +26,7 @@ }, "dependencies": { "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", - "Microsoft.Dnx.Runtime.Abstractions": "1.0.0-beta8" + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*" }, "resource": [ "Content/**/*" diff --git a/global.json b/global.json index 6a7d89b0d292..9c71b05c0cfb 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,6 @@ { - "projects": ["."] + "projects": ["."], + "sdk": { + "version": "1.0.0-rc1-final" + } } diff --git a/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs b/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs index d63ff692b5af..ffecfc5ac057 100644 --- a/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs +++ b/samples/angular/MusicStore/Apis/Models/MusicStoreContext.cs @@ -1,10 +1,5 @@ -using System; -using System.Linq; -using Microsoft.AspNet.Identity; -using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Data.Entity; -using Microsoft.Data.Entity.Metadata; -using Microsoft.Framework.OptionsModel; namespace MusicStore.Models { diff --git a/samples/angular/MusicStore/Apis/Models/SampleData.cs b/samples/angular/MusicStore/Apis/Models/SampleData.cs index 25e2a5c97f5a..c6e86d3de8c5 100644 --- a/samples/angular/MusicStore/Apis/Models/SampleData.cs +++ b/samples/angular/MusicStore/Apis/Models/SampleData.cs @@ -6,11 +6,8 @@ using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Data.Entity; -using Microsoft.Data.Entity.Storage; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.OptionsModel; -using MusicStore; -using MusicStore.Models; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.OptionsModel; namespace MusicStore.Models { diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index 81a5279c4c70..7e4d7148b207 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -1,18 +1,13 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using AutoMapper; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Data.Entity; -using Microsoft.Dnx.Runtime; -using Microsoft.Framework.Configuration; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.PlatformAbstractions; +using AutoMapper; using MusicStore.Apis; using MusicStore.Models; @@ -86,7 +81,7 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF // Initialize the sample data SampleData.InitializeMusicStoreDatabaseAsync(app.ApplicationServices).Wait(); - loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.MinimumLevel = LogLevel.Warning; loggerFactory.AddConsole(); loggerFactory.AddDebug(); diff --git a/samples/angular/MusicStore/project.json b/samples/angular/MusicStore/project.json index db764ce209bd..c303a1f824e0 100755 --- a/samples/angular/MusicStore/project.json +++ b/samples/angular/MusicStore/project.json @@ -5,21 +5,19 @@ "defaultNamespace": "MusicStore" }, "dependencies": { - "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", - "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", - "Microsoft.AspNet.Mvc": "6.0.0-beta8", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", - "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", - "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", - "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", - "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", - "Microsoft.Framework.Logging": "1.0.0-beta8", - "Microsoft.Framework.Logging.Console": "1.0.0-beta8", - "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", - "EntityFramework.SQLite": "7.0.0-beta8", - "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta8", - "AutoMapper": "4.0.0-alpha1", - "Microsoft.AspNet.AngularServices": "1.0.0-alpha7" + "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-*", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-*", + "Microsoft.AspNet.Mvc": "6.0.0-rc1-*", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-*", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-*", + "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-*", + "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-*", + "EntityFramework.SQLite": "7.0.0-rc1-*", + "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-rc1-*", + "Microsoft.AspNet.AngularServices": "1.0.0-alpha7", + "AutoMapper": "4.1.1" }, "commands": { "web": "Microsoft.AspNet.Server.Kestrel" diff --git a/samples/misc/ES2015Transpilation/Startup.cs b/samples/misc/ES2015Transpilation/Startup.cs index 80e7e5f4617e..376cadb06c53 100755 --- a/samples/misc/ES2015Transpilation/Startup.cs +++ b/samples/misc/ES2015Transpilation/Startup.cs @@ -1,11 +1,11 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; -using Microsoft.Dnx.Runtime; -using Microsoft.Framework.Configuration; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.Logging; using Microsoft.AspNet.NodeServices; using Microsoft.AspNet.Http; +using Microsoft.Extensions.PlatformAbstractions; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace ES2015Example { @@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services) // Configure is called after ConfigureServices is called. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, INodeServices nodeServices) { - loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.MinimumLevel = LogLevel.Warning; loggerFactory.AddConsole(); loggerFactory.AddDebug(); diff --git a/samples/misc/ES2015Transpilation/project.json b/samples/misc/ES2015Transpilation/project.json index 11769790b981..fcf2aa4d640b 100755 --- a/samples/misc/ES2015Transpilation/project.json +++ b/samples/misc/ES2015Transpilation/project.json @@ -5,17 +5,15 @@ "defaultNamespace": "ES2015Example" }, "dependencies": { - "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", - "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", - "Microsoft.AspNet.Mvc": "6.0.0-beta8", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", - "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", - "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", - "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", - "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", - "Microsoft.Framework.Logging": "1.0.0-beta8", - "Microsoft.Framework.Logging.Console": "1.0.0-beta8", - "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", + "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-*", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-*", + "Microsoft.AspNet.Mvc": "6.0.0-rc1-*", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-*", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-*", + "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-*", + "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-*", "Microsoft.AspNet.NodeServices": "1.0.0-alpha7" }, "commands": { diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs index 48ccde8bfb5b..0279cf6a7567 100755 --- a/samples/react/ReactGrid/Startup.cs +++ b/samples/react/ReactGrid/Startup.cs @@ -1,10 +1,9 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.NodeServices; -using Microsoft.Dnx.Runtime; -using Microsoft.Framework.Configuration; -using Microsoft.Framework.DependencyInjection; -using Microsoft.Framework.Logging; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.PlatformAbstractions; namespace ReactExample { @@ -32,7 +31,7 @@ public void ConfigureServices(IServiceCollection services) // Configure is called after ConfigureServices is called. public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { - loggerFactory.MinimumLevel = LogLevel.Information; + loggerFactory.MinimumLevel = LogLevel.Warning; loggerFactory.AddConsole(); loggerFactory.AddDebug(); diff --git a/samples/react/ReactGrid/project.json b/samples/react/ReactGrid/project.json index 167a3a0779d8..87370b98541e 100755 --- a/samples/react/ReactGrid/project.json +++ b/samples/react/ReactGrid/project.json @@ -5,17 +5,15 @@ "defaultNamespace": "ReactExample" }, "dependencies": { - "Microsoft.AspNet.Diagnostics": "1.0.0-beta8", - "Microsoft.AspNet.IISPlatformHandler": "1.0.0-beta8", - "Microsoft.AspNet.Mvc": "6.0.0-beta8", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta8", - "Microsoft.AspNet.Server.Kestrel": "1.0.0-beta8", - "Microsoft.AspNet.StaticFiles": "1.0.0-beta8", - "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta8", - "Microsoft.Framework.Configuration.Json": "1.0.0-beta8", - "Microsoft.Framework.Logging": "1.0.0-beta8", - "Microsoft.Framework.Logging.Console": "1.0.0-beta8", - "Microsoft.Framework.Logging.Debug": "1.0.0-beta8", + "Microsoft.AspNet.Diagnostics": "1.0.0-rc1-*", + "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-*", + "Microsoft.AspNet.Mvc": "6.0.0-rc1-*", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*", + "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-*", + "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-*", + "Microsoft.AspNet.Tooling.Razor": "1.0.0-rc1-*", + "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-*", + "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-*", "Microsoft.AspNet.ReactServices": "1.0.0-alpha7" }, "commands": { From d4692b0c4b382c8eb5a545b94e16ae95d45f50b6 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 24 Nov 2015 12:48:52 +0000 Subject: [PATCH 0027/1585] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0974e4951b88..a989fd6be2c7 100644 --- a/README.md +++ b/README.md @@ -67,10 +67,10 @@ To get started, **Using dnx on Windows/Mac/Linux** -1. Ensure you are using a suitable .NET runtime. Currently, this project is tested with version `1.0.0-beta8` on `coreclr`: +1. Ensure you are using a suitable .NET runtime. Currently, this project is tested with version `1.0.0-rc1-final` on `coreclr`: ``` - dnvm use 1.0.0-beta8 -r coreclr + dnvm use 1.0.0-rc1-final -r coreclr ``` 2. In the solution root directory (`NodeServices` - i.e., the directory that contains `NodeServices.sln`), restore the .NET dependencies: From 4ba80c32100c2fea86054bb8c4c729ed6eea133e Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 24 Nov 2015 14:20:14 +0000 Subject: [PATCH 0028/1585] Fix styling in React example --- samples/react/ReactGrid/ReactApp/boot-client.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/react/ReactGrid/ReactApp/boot-client.jsx b/samples/react/ReactGrid/ReactApp/boot-client.jsx index 42e4f9012718..b50fcf30c97c 100644 --- a/samples/react/ReactGrid/ReactApp/boot-client.jsx +++ b/samples/react/ReactGrid/ReactApp/boot-client.jsx @@ -2,6 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import createBrowserHistory from 'history/lib/createBrowserHistory'; import ReactApp from './components/ReactApp.jsx'; +import 'bootstrap/dist/css/bootstrap.css'; // In the browser, we render into a DOM node and hook up to the browser's history APIs var history = createBrowserHistory(); From dbc17acc624276e028d73bf17acb738eb3682e59 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 24 Nov 2015 14:33:22 +0000 Subject: [PATCH 0029/1585] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a989fd6be2c7..87feb7d238d5 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,8 @@ To get started, 3. If you get the error `'reactivex/rxjs' is not in the npm registry`, then your Visual Studio installation's version of the NPM tool is out of date. You will need to restore NPM dependencies manually from a command prompt (e.g., `cd samples\angular\MusicStore` then `npm install`). 4. Select a sample and run it. For example, right-click on the `MusicStore` project in Solution Explorer and choose `Set as startup project`. Then press `Ctrl+F5` to launch it. +Note that to run the React example, you'll also need to run `webpack` from the `samples\react\ReactGrid` directory (having first installed webpack if you don't yet have it - `npm install -g webpack`). + **Using dnx on Windows/Mac/Linux** 1. Ensure you are using a suitable .NET runtime. Currently, this project is tested with version `1.0.0-rc1-final` on `coreclr`: @@ -88,11 +90,9 @@ To get started, npm install ``` -4. Build the project - - ``` - gulp - ``` +4. Where applicable, build the project. For example, the Angular example uses Gulp, so you'll need to execute `gulp`, whereas the React example uses Webpack, so you'll need to execute `webpack`. The ES2015 example does not need to be built. + + If you don't already have it, install the applicable build tool first (e.g., `npm install -g webpack`). 5. Run the project (and wait until it displays the message `Application started`) From 7ac072781396ed58c7cab05b249c057e3186ea7f Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 25 Nov 2015 17:44:32 +0000 Subject: [PATCH 0030/1585] Towards working forms --- .../MusicStore/Apis/AlbumsApiController.cs | 3 +- .../MusicStore/Infrastructure/ApiResult.cs | 22 +++--------- .../admin/album-edit/album-edit.html | 26 +++++++------- .../components/admin/album-edit/album-edit.ts | 35 ++++++++----------- 4 files changed, 34 insertions(+), 52 deletions(-) diff --git a/samples/angular/MusicStore/Apis/AlbumsApiController.cs b/samples/angular/MusicStore/Apis/AlbumsApiController.cs index fdb44647d43d..dbd945978b12 100644 --- a/samples/angular/MusicStore/Apis/AlbumsApiController.cs +++ b/samples/angular/MusicStore/Apis/AlbumsApiController.cs @@ -113,8 +113,7 @@ public async Task CreateAlbum([FromBody]AlbumChangeDto album) } [HttpPut("{albumId:int}/update")] - [Authorize("app-ManageStore")] - public async Task UpdateAlbum(int albumId, [FromBody]AlbumChangeDto album) + public async Task UpdateAlbum(int albumId, [FromBody] AlbumChangeDto album) { if (!ModelState.IsValid) { diff --git a/samples/angular/MusicStore/Infrastructure/ApiResult.cs b/samples/angular/MusicStore/Infrastructure/ApiResult.cs index 9aae7804e69b..7adac5947e9a 100644 --- a/samples/angular/MusicStore/Infrastructure/ApiResult.cs +++ b/samples/angular/MusicStore/Infrastructure/ApiResult.cs @@ -1,7 +1,6 @@ using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.ModelBinding; using Newtonsoft.Json; -using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -13,16 +12,13 @@ public class ApiResult : ActionResult public ApiResult(ModelStateDictionary modelState) : this() { - if (modelState.Any(m => m.Value.Errors.Count > 0)) + if (modelState.Any(m => m.Value.Errors.Any())) { StatusCode = 400; Message = "The model submitted was invalid. Please correct the specified errors and try again."; ModelErrors = modelState - .SelectMany(m => m.Value.Errors.Select(me => new ModelError - { - FieldName = m.Key, - ErrorMessage = me.ErrorMessage - })); + .Where(m => m.Value.Errors.Any()) + .ToDictionary(m => m.Key, m => m.Value.Errors.Select(me => me.ErrorMessage )); } } @@ -40,7 +36,7 @@ public ApiResult() public object Data { get; set; } [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IEnumerable ModelErrors { get; set; } + public IDictionary> ModelErrors { get; set; } public override Task ExecuteResultAsync(ActionContext context) { @@ -49,15 +45,7 @@ public override Task ExecuteResultAsync(ActionContext context) context.HttpContext.Response.StatusCode = StatusCode.Value; } - var json = new JsonResult(this); - return json.ExecuteResultAsync(context); - } - - public class ModelError - { - public string FieldName { get; set; } - - public string ErrorMessage { get; set; } + return new ObjectResult(this).ExecuteResultAsync(context); } } } diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html index 1276a38f0dde..393f80bd496f 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html @@ -2,37 +2,37 @@

Album Edit


- - + - - + - - + + - +
$ - +
- - + + - + diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index e199e22461a9..83a40b0f9775 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -23,14 +23,15 @@ export class AlbumEdit { constructor(fb: ng.FormBuilder, http: Http, routeParam: router.RouteParams) { this._http = http; - http.get('/api/albums/' + routeParam.params['albumId']).subscribe(result => { + var albumId = parseInt(routeParam.params['albumId']); + http.get('/api/albums/' + albumId).subscribe(result => { var json = result.json(); this.originalAlbum = json; - (this.form.controls['title']).updateValue(json.Title); - (this.form.controls['price']).updateValue(json.Price); - (this.form.controls['artist']).updateValue(json.ArtistId); - (this.form.controls['genre']).updateValue(json.GenreId); - (this.form.controls['albumArtUrl']).updateValue(json.AlbumArtUrl); + (this.form.controls['Title']).updateValue(json.Title); + (this.form.controls['Price']).updateValue(json.Price); + (this.form.controls['ArtistId']).updateValue(json.ArtistId); + (this.form.controls['GenreId']).updateValue(json.GenreId); + (this.form.controls['AlbumArtUrl']).updateValue(json.AlbumArtUrl); }); http.get('/api/artists/lookup').subscribe(result => { @@ -42,11 +43,12 @@ export class AlbumEdit { }); this.form = fb.group({ - artist: fb.control('', ng.Validators.required), - genre: fb.control('', ng.Validators.required), - title: fb.control('', ng.Validators.required), - price: fb.control('', ng.Validators.compose([ng.Validators.required, AlbumEdit._validatePrice])), - albumArtUrl: fb.control('', ng.Validators.required) + AlbumId: fb.control(albumId), + ArtistId: fb.control(0, ng.Validators.required), + GenreId: fb.control(0, ng.Validators.required), + Title: fb.control('', ng.Validators.required), + Price: fb.control('', ng.Validators.compose([ng.Validators.required, AlbumEdit._validatePrice])), + AlbumArtUrl: fb.control('', ng.Validators.required) }); } @@ -62,14 +64,7 @@ export class AlbumEdit { (window).fetch(`/api/albums/${ albumId }/update`, { method: 'put', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - AlbumArtUrl: controls['albumArtUrl'].value, - AlbumId: albumId, - ArtistId: controls['artist'].value, - GenreId: controls['genre'].value, - Price: controls['price'].value, - Title: controls['title'].value - }) + body: JSON.stringify(this.form.value) }).then(response => { console.log(response); }); @@ -77,6 +72,6 @@ export class AlbumEdit { } private static _validatePrice(control: ng.Control): { [key: string]: boolean } { - return /^\d+\.\d+$/.test(control.value) ? null : { price: true }; + return /^\d+\.\d+$/.test(control.value) ? null : { Price: true }; } } From 74bae91c3acfae60e87c16117103ffca690774cf Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 25 Nov 2015 19:15:38 +0000 Subject: [PATCH 0031/1585] Rough but working example of displaying server-side validation errors. Needs cleaner patterns/APIs. --- .../components/admin/album-edit/album-edit.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index 83a40b0f9775..117d538db04c 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -5,6 +5,8 @@ import { Http, HTTP_BINDINGS, Headers } from 'angular2/http'; import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; import { FormField } from '../form-field/form-field'; +const ContentTypeJson = 'application/json'; + @ng.Component({ selector: 'album-edit' }) @@ -63,10 +65,29 @@ export class AlbumEdit { var albumId = this.originalAlbum.AlbumId; (window).fetch(`/api/albums/${ albumId }/update`, { method: 'put', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': ContentTypeJson }, body: JSON.stringify(this.form.value) }).then(response => { - console.log(response); + if (response.status >= 200 && response.status < 300) { + // TODO: Display success message + } else { + if (response.headers.get('Content-Type').indexOf(ContentTypeJson) === 0) { + return response.json().then((responseJson: ValidationResponse) => { + Object.keys(responseJson.ModelErrors).forEach(key => { + responseJson.ModelErrors[key].forEach(errorMessage => { + // TODO: There has to be a better API for this + if (!this.form.controls[key].errors) { + (this.form.controls[key])._errors = {}; + } + + this.form.controls[key].errors[errorMessage] = true; + }); + }); + }); + } else { + // TODO: Display generic 'unknown error' + } + } }); } } @@ -75,3 +96,8 @@ export class AlbumEdit { return /^\d+\.\d+$/.test(control.value) ? null : { Price: true }; } } + +interface ValidationResponse { + Message: string; + ModelErrors: { [key: string]: string[] }; +} From 6f840b60cadabdb3c566a27f730e0df1e0d884bc Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 26 Nov 2015 10:57:08 +0000 Subject: [PATCH 0032/1585] Improvements to form UI --- .../admin/album-edit/album-edit.html | 1 + .../components/admin/album-edit/album-edit.ts | 53 ++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html index 393f80bd496f..f32b27a535b9 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html @@ -36,6 +36,7 @@

Album Edit

+
Done! Your changes were saved.
Back to List diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index 117d538db04c..6119d9f0f3b5 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -1,12 +1,10 @@ import * as ng from 'angular2/angular2'; import * as router from 'angular2/router'; import * as models from '../../../models/models'; -import { Http, HTTP_BINDINGS, Headers } from 'angular2/http'; +import { Http, HTTP_BINDINGS, Headers, Response } from 'angular2/http'; import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; import { FormField } from '../form-field/form-field'; -const ContentTypeJson = 'application/json'; - @ng.Component({ selector: 'album-edit' }) @@ -19,6 +17,7 @@ export class AlbumEdit { public artists: models.Artist[]; public genres: models.Genre[]; public originalAlbum: models.Album; + public changesSaved: boolean; private _http: Http; @@ -52,6 +51,10 @@ export class AlbumEdit { Price: fb.control('', ng.Validators.compose([ng.Validators.required, AlbumEdit._validatePrice])), AlbumArtUrl: fb.control('', ng.Validators.required) }); + + this.form.valueChanges.observer({ + next: _ => { this.changesSaved = false; } + }); } public onSubmitModelBased() { @@ -63,30 +66,22 @@ export class AlbumEdit { if (this.form.valid) { var controls = this.form.controls; var albumId = this.originalAlbum.AlbumId; - (window).fetch(`/api/albums/${ albumId }/update`, { - method: 'put', - headers: { 'Content-Type': ContentTypeJson }, - body: JSON.stringify(this.form.value) - }).then(response => { - if (response.status >= 200 && response.status < 300) { - // TODO: Display success message + + this._putJson(`/api/albums/${ albumId }/update`, this.form.value).then(response => { + if (response.status === 200) { + this.changesSaved = true; } else { - if (response.headers.get('Content-Type').indexOf(ContentTypeJson) === 0) { - return response.json().then((responseJson: ValidationResponse) => { - Object.keys(responseJson.ModelErrors).forEach(key => { - responseJson.ModelErrors[key].forEach(errorMessage => { - // TODO: There has to be a better API for this - if (!this.form.controls[key].errors) { - (this.form.controls[key])._errors = {}; - } - - this.form.controls[key].errors[errorMessage] = true; - }); - }); + var errors = ((response.json())).ModelErrors; + Object.keys(errors).forEach(key => { + errors[key].forEach(errorMessage => { + // TODO: There has to be a better API for this + if (!this.form.controls[key].errors) { + (this.form.controls[key])._errors = {}; + } + + this.form.controls[key].errors[errorMessage] = true; }); - } else { - // TODO: Display generic 'unknown error' - } + }); } }); } @@ -95,6 +90,14 @@ export class AlbumEdit { private static _validatePrice(control: ng.Control): { [key: string]: boolean } { return /^\d+\.\d+$/.test(control.value) ? null : { Price: true }; } + + private _putJson(url: string, body: any): Promise { + return new Promise((resolve, reject) => { + this._http.put(url, JSON.stringify(body), { + headers: new Headers({ 'Content-Type': 'application/json' }) + }).subscribe(resolve); + }); + } } interface ValidationResponse { From ad04dd175089b050afd34b89ac9be7bd1557d5f9 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 26 Nov 2015 11:47:12 +0000 Subject: [PATCH 0033/1585] Work around CoreCLR issue --- samples/angular/MusicStore/Apis/Models/Album.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/angular/MusicStore/Apis/Models/Album.cs b/samples/angular/MusicStore/Apis/Models/Album.cs index 35f03d9f821e..919bf1b1f74e 100644 --- a/samples/angular/MusicStore/Apis/Models/Album.cs +++ b/samples/angular/MusicStore/Apis/Models/Album.cs @@ -23,7 +23,7 @@ public Album() public string Title { get; set; } [Required] - [Range(0.01, 100.00)] + [RangeAttribute(typeof(double), "0.01", "100")] // Long-form constructor to work around https://github.com/dotnet/coreclr/issues/2172 [DataType(DataType.Currency)] public decimal Price { get; set; } From 4a5b9e62eff7255da277cb15b0765b5991bdc2b7 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 9 Dec 2015 14:53:43 +0000 Subject: [PATCH 0034/1585] Add MapSpaFallbackRoute helper. Will move into separate package shortly. --- samples/angular/MusicStore/Startup.cs | 73 +++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 5 deletions(-) diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index 7e4d7148b207..e49748e8ce66 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -1,7 +1,11 @@ +using System; +using System.Collections.Generic; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; +using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity.EntityFramework; +using Microsoft.AspNet.Routing; using Microsoft.Data.Entity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -108,15 +112,74 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF // Add MVC to the request pipeline. app.UseMvc(routes => { - routes.MapRoute( - name: "default", - template: "{controller=Home}/{action=Index}/{id?}"); - - routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" }); + // Matches any request that doesn't appear to have a filename extension (defined as 'having a dot in the last URI segment'). + // This means you'll correctly get 404s for /some/dir/non-existent-image.png instead of returning the SPA HTML. + // However, it means requests like /customers/isaac.newton will *not* be mapped into the SPA, so if you need to accept + // URIs like that you'll need to match all URIs, e.g.: + // routes.MapbackRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" }); + // (which of course will match /customers/isaac.png too - maybe that is a real customer name, not a PNG image). + routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); // Uncomment the following line to add a route for porting Web API 2 controllers. // routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}"); }); } } + internal static class SpaRouteExtensions { + private const string ClientRouteTokenName = "clientRoute"; + + public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null) { + MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens); + } + + public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null) + { + var template = CreateRouteTemplate(templatePrefix); + + var constraintsDict = ObjectToDictionary(constraints); + constraintsDict.Add(ClientRouteTokenName, new SpaRouteConstraint()); + + routeBuilder.MapRoute(name, template, defaults, constraintsDict, dataTokens); + } + + private static string CreateRouteTemplate(string templatePrefix) + { + templatePrefix = templatePrefix ?? string.Empty; + + if (templatePrefix.Contains("?")) { + // TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?' + throw new ArgumentException("SPA fallback route templates don't support querystrings"); + } + + if (templatePrefix.Contains("#")) { + throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); + } + + if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) { + templatePrefix += "/"; + } + + return templatePrefix + $"{{*{ ClientRouteTokenName }}}"; + } + + private static IDictionary ObjectToDictionary(object value) + { + return value as IDictionary ?? new RouteValueDictionary(value); + } + + private class SpaRouteConstraint : IRouteConstraint + { + public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary values, RouteDirection routeDirection) + { + var clientRouteValue = (values[ClientRouteTokenName] as string) ?? string.Empty; + return !HasDotInLastSegment(clientRouteValue); + } + + private bool HasDotInLastSegment(string uri) + { + var lastSegmentStartPos = uri.LastIndexOf('/'); + return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; + } + } + } } From d4f04d211fefb9a26e2ace4728b5227aec0bec13 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 9 Dec 2015 15:13:56 +0000 Subject: [PATCH 0035/1585] Create a new shared package, Microsoft.AspNet.SpaServices, to hold MapSpaFallbackRoute (and other common infrastructure yet to be added) --- Microsoft.AspNet.AngularServices/project.json | 3 +- Microsoft.AspNet.SpaServices/.gitignore | 1 + .../SpaRouteConstraint.cs | 32 ++++++++++ .../SpaRouteExtensions.cs | 53 +++++++++++++++++ Microsoft.AspNet.SpaServices/project.json | 22 +++++++ samples/angular/MusicStore/Startup.cs | 59 ------------------- 6 files changed, 110 insertions(+), 60 deletions(-) create mode 100644 Microsoft.AspNet.SpaServices/.gitignore create mode 100644 Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs create mode 100644 Microsoft.AspNet.SpaServices/SpaRouteExtensions.cs create mode 100644 Microsoft.AspNet.SpaServices/project.json diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index 41c8f1aa07e7..01fbeb691a2d 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -26,7 +26,8 @@ }, "dependencies": { "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*" + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*", + "Microsoft.AspNet.SpaServices": "1.0.0-alpha7" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.SpaServices/.gitignore b/Microsoft.AspNet.SpaServices/.gitignore new file mode 100644 index 000000000000..ae3c1726048c --- /dev/null +++ b/Microsoft.AspNet.SpaServices/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs b/Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs new file mode 100644 index 000000000000..d4df4c77fd21 --- /dev/null +++ b/Microsoft.AspNet.SpaServices/SpaRouteConstraint.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Routing; + +namespace Microsoft.AspNet.SpaServices +{ + internal class SpaRouteConstraint : IRouteConstraint + { + private readonly string clientRouteTokenName; + + public SpaRouteConstraint(string clientRouteTokenName) { + if (string.IsNullOrEmpty(clientRouteTokenName)) { + throw new ArgumentException("Value cannot be null or empty", "clientRouteTokenName"); + } + + this.clientRouteTokenName = clientRouteTokenName; + } + + public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary values, RouteDirection routeDirection) + { + var clientRouteValue = (values[this.clientRouteTokenName] as string) ?? string.Empty; + return !HasDotInLastSegment(clientRouteValue); + } + + private bool HasDotInLastSegment(string uri) + { + var lastSegmentStartPos = uri.LastIndexOf('/'); + return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; + } + } +} diff --git a/Microsoft.AspNet.SpaServices/SpaRouteExtensions.cs b/Microsoft.AspNet.SpaServices/SpaRouteExtensions.cs new file mode 100644 index 000000000000..a471204f1d84 --- /dev/null +++ b/Microsoft.AspNet.SpaServices/SpaRouteExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNet.Routing; +using Microsoft.AspNet.SpaServices; + +// Putting in this namespace so it's always available whenever MapRoute is +namespace Microsoft.AspNet.Builder +{ + public static class SpaRouteExtensions + { + private const string ClientRouteTokenName = "clientRoute"; + + public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null) + { + MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens); + } + + public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null) + { + var template = CreateRouteTemplate(templatePrefix); + + var constraintsDict = ObjectToDictionary(constraints); + constraintsDict.Add(ClientRouteTokenName, new SpaRouteConstraint(ClientRouteTokenName)); + + routeBuilder.MapRoute(name, template, defaults, constraintsDict, dataTokens); + } + + private static string CreateRouteTemplate(string templatePrefix) + { + templatePrefix = templatePrefix ?? string.Empty; + + if (templatePrefix.Contains("?")) { + // TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?' + throw new ArgumentException("SPA fallback route templates don't support querystrings"); + } + + if (templatePrefix.Contains("#")) { + throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); + } + + if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) { + templatePrefix += "/"; + } + + return templatePrefix + $"{{*{ ClientRouteTokenName }}}"; + } + + private static IDictionary ObjectToDictionary(object value) + { + return value as IDictionary ?? new RouteValueDictionary(value); + } + } +} diff --git a/Microsoft.AspNet.SpaServices/project.json b/Microsoft.AspNet.SpaServices/project.json new file mode 100644 index 000000000000..19c848c5eec0 --- /dev/null +++ b/Microsoft.AspNet.SpaServices/project.json @@ -0,0 +1,22 @@ +{ + "version": "1.0.0-alpha7", + "description": "Microsoft.AspNet.SpaServices", + "authors": [ + "Microsoft" + ], + "tags": [ + "" + ], + "projectUrl": "", + "licenseUrl": "", + "dependencies": { + "Microsoft.AspNet.Routing": "1.0.0-rc1-*" + }, + "frameworks": { + "dnx451": {}, + "dnxcore50": { + "dependencies": { + } + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index e49748e8ce66..eda89585a4b1 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -3,9 +3,7 @@ using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Identity.EntityFramework; -using Microsoft.AspNet.Routing; using Microsoft.Data.Entity; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -125,61 +123,4 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF }); } } - internal static class SpaRouteExtensions { - private const string ClientRouteTokenName = "clientRoute"; - - public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, object defaults, object constraints = null, object dataTokens = null) { - MapSpaFallbackRoute(routeBuilder, name, /* templatePrefix */ (string)null, defaults, constraints, dataTokens); - } - - public static void MapSpaFallbackRoute(this IRouteBuilder routeBuilder, string name, string templatePrefix, object defaults, object constraints = null, object dataTokens = null) - { - var template = CreateRouteTemplate(templatePrefix); - - var constraintsDict = ObjectToDictionary(constraints); - constraintsDict.Add(ClientRouteTokenName, new SpaRouteConstraint()); - - routeBuilder.MapRoute(name, template, defaults, constraintsDict, dataTokens); - } - - private static string CreateRouteTemplate(string templatePrefix) - { - templatePrefix = templatePrefix ?? string.Empty; - - if (templatePrefix.Contains("?")) { - // TODO: Consider supporting this. The {*clientRoute} part should be added immediately before the '?' - throw new ArgumentException("SPA fallback route templates don't support querystrings"); - } - - if (templatePrefix.Contains("#")) { - throw new ArgumentException("SPA fallback route templates should not include # characters. The hash part of a URI does not get sent to the server."); - } - - if (templatePrefix != string.Empty && !templatePrefix.EndsWith("/")) { - templatePrefix += "/"; - } - - return templatePrefix + $"{{*{ ClientRouteTokenName }}}"; - } - - private static IDictionary ObjectToDictionary(object value) - { - return value as IDictionary ?? new RouteValueDictionary(value); - } - - private class SpaRouteConstraint : IRouteConstraint - { - public bool Match(HttpContext httpContext, IRouter route, string routeKey, IDictionary values, RouteDirection routeDirection) - { - var clientRouteValue = (values[ClientRouteTokenName] as string) ?? string.Empty; - return !HasDotInLastSegment(clientRouteValue); - } - - private bool HasDotInLastSegment(string uri) - { - var lastSegmentStartPos = uri.LastIndexOf('/'); - return uri.IndexOf('.', lastSegmentStartPos + 1) >= 0; - } - } - } } From 78efc77be47b710868ce0994299fee921a1afae5 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 9 Dec 2015 15:17:31 +0000 Subject: [PATCH 0036/1585] Use MapSpaFallbackRoute in ReactServices and demo --- Microsoft.AspNet.ReactServices/project.json | 3 ++- samples/react/ReactGrid/Controllers/HomeController.cs | 3 +-- samples/react/ReactGrid/Startup.cs | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Microsoft.AspNet.ReactServices/project.json b/Microsoft.AspNet.ReactServices/project.json index c6904f25b994..98cf6ebb836d 100644 --- a/Microsoft.AspNet.ReactServices/project.json +++ b/Microsoft.AspNet.ReactServices/project.json @@ -26,7 +26,8 @@ }, "dependencies": { "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", - "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*" + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-*", + "Microsoft.AspNet.SpaServices": "1.0.0-alpha7" }, "resource": [ "Content/**/*" diff --git a/samples/react/ReactGrid/Controllers/HomeController.cs b/samples/react/ReactGrid/Controllers/HomeController.cs index 4a9899cdc446..14063a5b272c 100755 --- a/samples/react/ReactGrid/Controllers/HomeController.cs +++ b/samples/react/ReactGrid/Controllers/HomeController.cs @@ -1,11 +1,10 @@ -using System.Threading.Tasks; using Microsoft.AspNet.Mvc; namespace ReactExample.Controllers { public class HomeController : Controller { - public IActionResult Index(int pageIndex) + public IActionResult Index() { return View(); } diff --git a/samples/react/ReactGrid/Startup.cs b/samples/react/ReactGrid/Startup.cs index 0279cf6a7567..244bbfe06f4f 100755 --- a/samples/react/ReactGrid/Startup.cs +++ b/samples/react/ReactGrid/Startup.cs @@ -58,9 +58,8 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF // Add MVC to the request pipeline. app.UseMvc(routes => { - routes.MapRoute( + routes.MapSpaFallbackRoute( name: "default", - template: "{pageIndex?}", defaults: new { controller="Home", action = "Index" }); }); } From 906a17ea3c222829eca7294285bfada172780693 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 9 Dec 2015 17:30:35 +0000 Subject: [PATCH 0037/1585] Define ValidationErrorResult in SpaServices; use it in MusicStore --- .../ValidationErrorResult.cs | 30 +++++++++++ Microsoft.AspNet.SpaServices/project.json | 1 + .../MusicStore/Apis/AlbumsApiController.cs | 26 ++++------ .../MusicStore/Infrastructure/ApiResult.cs | 51 ------------------- .../components/admin/album-edit/album-edit.ts | 5 +- 5 files changed, 44 insertions(+), 69 deletions(-) create mode 100644 Microsoft.AspNet.SpaServices/ValidationErrorResult.cs delete mode 100644 samples/angular/MusicStore/Infrastructure/ApiResult.cs diff --git a/Microsoft.AspNet.SpaServices/ValidationErrorResult.cs b/Microsoft.AspNet.SpaServices/ValidationErrorResult.cs new file mode 100644 index 000000000000..9cd0ba1d458d --- /dev/null +++ b/Microsoft.AspNet.SpaServices/ValidationErrorResult.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.Mvc.ModelBinding; + +namespace Microsoft.AspNet.SpaServices +{ + public class ValidationErrorResult : ObjectResult { + public const int DefaultStatusCode = 400; + + public ValidationErrorResult(ModelStateDictionary modelState, int errorStatusCode = DefaultStatusCode) + : base(CreateResultObject(modelState)) + { + if (!modelState.IsValid) { + this.StatusCode = errorStatusCode; + } + } + + private static IDictionary> CreateResultObject(ModelStateDictionary modelState) + { + if (modelState.IsValid) { + return null; + } else { + return modelState + .Where(m => m.Value.Errors.Any()) + .ToDictionary(m => m.Key, m => m.Value.Errors.Select(me => me.ErrorMessage)); + } + } + } +} diff --git a/Microsoft.AspNet.SpaServices/project.json b/Microsoft.AspNet.SpaServices/project.json index 19c848c5eec0..4813c74fcf63 100644 --- a/Microsoft.AspNet.SpaServices/project.json +++ b/Microsoft.AspNet.SpaServices/project.json @@ -10,6 +10,7 @@ "projectUrl": "", "licenseUrl": "", "dependencies": { + "Microsoft.AspNet.Mvc": "6.0.0-rc1-*", "Microsoft.AspNet.Routing": "1.0.0-rc1-*" }, "frameworks": { diff --git a/samples/angular/MusicStore/Apis/AlbumsApiController.cs b/samples/angular/MusicStore/Apis/AlbumsApiController.cs index dbd945978b12..3b57e0b5690d 100644 --- a/samples/angular/MusicStore/Apis/AlbumsApiController.cs +++ b/samples/angular/MusicStore/Apis/AlbumsApiController.cs @@ -6,6 +6,7 @@ using AutoMapper; using MusicStore.Models; using MusicStore.Infrastructure; +using Microsoft.AspNet.SpaServices; namespace MusicStore.Apis { @@ -95,7 +96,7 @@ public async Task CreateAlbum([FromBody]AlbumChangeDto album) if (!ModelState.IsValid) { // Return the model errors - return new ApiResult(ModelState); + return new ValidationErrorResult(ModelState); } // Save the changes to the DB @@ -105,11 +106,10 @@ public async Task CreateAlbum([FromBody]AlbumChangeDto album) // TODO: Handle missing record, key violations, concurrency issues, etc. - return new ApiResult - { + return new ObjectResult(new { Data = dbAlbum.AlbumId, Message = "Album created successfully." - }; + }); } [HttpPut("{albumId:int}/update")] @@ -118,18 +118,16 @@ public async Task UpdateAlbum(int albumId, [FromBody] AlbumChangeD if (!ModelState.IsValid) { // Return the model errors - return new ApiResult(ModelState); + return new ValidationErrorResult(ModelState); } var dbAlbum = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId); if (dbAlbum == null) { - return new ApiResult - { - StatusCode = 404, + return new ObjectResult(new { Message = string.Format("The album with ID {0} was not found.", albumId) - }; + }) { StatusCode = 404 }; } // Save the changes to the DB @@ -138,10 +136,9 @@ public async Task UpdateAlbum(int albumId, [FromBody] AlbumChangeD // TODO: Handle missing record, key violations, concurrency issues, etc. - return new ApiResult - { + return new ObjectResult (new { Message = "Album updated successfully." - }; + }); } [HttpDelete("{albumId:int}")] @@ -161,10 +158,9 @@ public async Task DeleteAlbum(int albumId) // TODO: Handle missing record, key violations, concurrency issues, etc. } - return new ApiResult - { + return new ObjectResult (new { Message = "Album deleted successfully." - }; + }); } } diff --git a/samples/angular/MusicStore/Infrastructure/ApiResult.cs b/samples/angular/MusicStore/Infrastructure/ApiResult.cs deleted file mode 100644 index 7adac5947e9a..000000000000 --- a/samples/angular/MusicStore/Infrastructure/ApiResult.cs +++ /dev/null @@ -1,51 +0,0 @@ -using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.Mvc.ModelBinding; -using Newtonsoft.Json; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace MusicStore.Infrastructure -{ - public class ApiResult : ActionResult - { - public ApiResult(ModelStateDictionary modelState) - : this() - { - if (modelState.Any(m => m.Value.Errors.Any())) - { - StatusCode = 400; - Message = "The model submitted was invalid. Please correct the specified errors and try again."; - ModelErrors = modelState - .Where(m => m.Value.Errors.Any()) - .ToDictionary(m => m.Key, m => m.Value.Errors.Select(me => me.ErrorMessage )); - } - } - - public ApiResult() - { - - } - - [JsonIgnore] - public int? StatusCode { get; set; } - - public string Message { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public object Data { get; set; } - - [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] - public IDictionary> ModelErrors { get; set; } - - public override Task ExecuteResultAsync(ActionContext context) - { - if (StatusCode.HasValue) - { - context.HttpContext.Response.StatusCode = StatusCode.Value; - } - - return new ObjectResult(this).ExecuteResultAsync(context); - } - } -} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index 6119d9f0f3b5..6aabf4ae7151 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -71,7 +71,7 @@ export class AlbumEdit { if (response.status === 200) { this.changesSaved = true; } else { - var errors = ((response.json())).ModelErrors; + var errors = (response.json()); Object.keys(errors).forEach(key => { errors[key].forEach(errorMessage => { // TODO: There has to be a better API for this @@ -101,6 +101,5 @@ export class AlbumEdit { } interface ValidationResponse { - Message: string; - ModelErrors: { [key: string]: string[] }; + [propertyName: string]: string[]; } From 2261c9964e3ac4e9da9ced32dfaf297d868b832c Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Wed, 9 Dec 2015 18:25:09 +0000 Subject: [PATCH 0038/1585] Preparing to move the ASP.NET MVC validation result client-side code into a separate NPM module --- .../components/admin/album-edit/AspNetUtil.ts | 32 +++++++++++++++++++ .../components/admin/album-edit/album-edit.ts | 29 ++++++----------- 2 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts new file mode 100644 index 000000000000..c90b4063e9ab --- /dev/null +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts @@ -0,0 +1,32 @@ +import { ControlGroup } from 'angular2/angular2'; +import { Response } from 'angular2/http'; + +// TODO: Factor this out into a separate NPM module +export class Validation { + + public static showValidationErrors(response: ValidationErrorResult | Response, controlGroup: ControlGroup): void { + if (response instanceof Response) { + var httpResponse = response; + response = (httpResponse.json()); + } + + // It's not yet clear whether this is a legitimate and supported use of the ng.ControlGroup API. + // Need feedback from the Angular 2 team on whether there's a better way. + var errors = response; + Object.keys(errors || {}).forEach(key => { + errors[key].forEach(errorMessage => { + // This in particular is rough + if (!controlGroup.controls[key].errors) { + (controlGroup.controls[key])._errors = {}; + } + + controlGroup.controls[key].errors[errorMessage] = true; + }); + }); + } + +} + +export interface ValidationErrorResult { + [propertyName: string]: string[]; +} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index 6aabf4ae7151..c7a943a86584 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -4,6 +4,7 @@ import * as models from '../../../models/models'; import { Http, HTTP_BINDINGS, Headers, Response } from 'angular2/http'; import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; import { FormField } from '../form-field/form-field'; +import * as AspNet from './AspNetUtil'; @ng.Component({ selector: 'album-edit' @@ -67,21 +68,11 @@ export class AlbumEdit { var controls = this.form.controls; var albumId = this.originalAlbum.AlbumId; - this._putJson(`/api/albums/${ albumId }/update`, this.form.value).then(response => { + this._putJson(`/api/albums/${ albumId }/update`, this.form.value).subscribe(response => { if (response.status === 200) { this.changesSaved = true; } else { - var errors = (response.json()); - Object.keys(errors).forEach(key => { - errors[key].forEach(errorMessage => { - // TODO: There has to be a better API for this - if (!this.form.controls[key].errors) { - (this.form.controls[key])._errors = {}; - } - - this.form.controls[key].errors[errorMessage] = true; - }); - }); + AspNet.Validation.showValidationErrors(response, this.form); } }); } @@ -91,15 +82,15 @@ export class AlbumEdit { return /^\d+\.\d+$/.test(control.value) ? null : { Price: true }; } - private _putJson(url: string, body: any): Promise { - return new Promise((resolve, reject) => { - this._http.put(url, JSON.stringify(body), { - headers: new Headers({ 'Content-Type': 'application/json' }) - }).subscribe(resolve); + // Need feedback on whether this really is the easiest way to PUT some JSON + private _putJson(url: string, body: any): Subscribable { + return this._http.put(url, JSON.stringify(body), { + headers: new Headers({ 'Content-Type': 'application/json' }) }); } } -interface ValidationResponse { - [propertyName: string]: string[]; +// TODO: Figure out what type declaration is provided by Angular/RxJs and use that instead +interface Subscribable { + subscribe(callback: (response: Response) => void): void; } From bc359a3a4bd8a477ae10057c1ee16426fb12e197 Mon Sep 17 00:00:00 2001 From: Greg Beaty Date: Wed, 9 Dec 2015 16:18:00 -0500 Subject: [PATCH 0039/1585] Replace express with native node calls --- Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js index f19c114b365f..b9478c27e632 100644 --- a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -20,7 +20,8 @@ var server = http.createServer(function(req, res) { if (!hasSentResult) { hasSentResult = true; if (errorValue) { - res.status(500).send(errorValue); + res.statusCode = 500; + res.end(errorValue.toString()); } else if (typeof successValue !== 'string') { // Arbitrary object/number/etc - JSON-serialize it res.setHeader('Content-Type', 'application/json'); From 1e446b67974311e7069b20c00e63630803497ed5 Mon Sep 17 00:00:00 2001 From: Greg Beaty Date: Wed, 9 Dec 2015 16:36:30 -0500 Subject: [PATCH 0040/1585] Add exception stack to error response if available --- .../Content/Node/entrypoint-http.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js index b9478c27e632..153d94f4b0f7 100644 --- a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -21,7 +21,12 @@ var server = http.createServer(function(req, res) { hasSentResult = true; if (errorValue) { res.statusCode = 500; - res.end(errorValue.toString()); + + if (errorValue.stack) { + res.end(errorValue.stack); + } else { + res.end(errorValue.toString()); + } } else if (typeof successValue !== 'string') { // Arbitrary object/number/etc - JSON-serialize it res.setHeader('Content-Type', 'application/json'); From f6bb28a71dd45505c6ceb15826473c9f1f237737 Mon Sep 17 00:00:00 2001 From: Greg Beaty Date: Wed, 9 Dec 2015 16:40:04 -0500 Subject: [PATCH 0041/1585] Add handling of error response from node process --- .../HostingModels/HttpNodeInstance.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs index b863ad1fb4d3..67075271cfb7 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs @@ -29,6 +29,11 @@ public override async Task Invoke(NodeInvocationInfo invocationInfo) { var payload = new StringContent(payloadJson, Encoding.UTF8, "application/json"); var response = await client.PostAsync("http://localhost:" + this._portNumber, payload); var responseString = await response.Content.ReadAsStringAsync(); + + if (response.StatusCode != HttpStatusCode.OK) { + throw new Exception("Node module responded with error: " + responseString); + } + var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; if (responseIsJson) { return JsonConvert.DeserializeObject(responseString); From cda1663d1e7756acca8a1ff9da13c76474dc2858 Mon Sep 17 00:00:00 2001 From: Greg Beaty Date: Thu, 10 Dec 2015 01:52:01 -0500 Subject: [PATCH 0042/1585] Add missing imports --- Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs index 67075271cfb7..d987df384f71 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs @@ -1,3 +1,5 @@ +using System; +using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; From 30281636d663a240aeeb5eee1f362e8458c57db9 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 10 Dec 2015 14:10:21 +0000 Subject: [PATCH 0043/1585] Beginning angular2-aspnet NPM package --- .../npm/.gitignore | 3 ++ .../npm/.npmignore | 3 ++ Microsoft.AspNet.AngularServices/npm/build.js | 32 +++++++++++++++++++ .../npm/package.json | 24 ++++++++++++++ .../npm/src/Exports.ts | 1 + .../npm/src/Validation.ts | 31 ++++++++++++++++++ .../npm/tsconfig.json | 13 ++++++++ 7 files changed, 107 insertions(+) create mode 100644 Microsoft.AspNet.AngularServices/npm/.gitignore create mode 100644 Microsoft.AspNet.AngularServices/npm/.npmignore create mode 100644 Microsoft.AspNet.AngularServices/npm/build.js create mode 100644 Microsoft.AspNet.AngularServices/npm/package.json create mode 100644 Microsoft.AspNet.AngularServices/npm/src/Exports.ts create mode 100644 Microsoft.AspNet.AngularServices/npm/src/Validation.ts create mode 100644 Microsoft.AspNet.AngularServices/npm/tsconfig.json diff --git a/Microsoft.AspNet.AngularServices/npm/.gitignore b/Microsoft.AspNet.AngularServices/npm/.gitignore new file mode 100644 index 000000000000..6cb02b18d986 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/dist/ +/bundles/ diff --git a/Microsoft.AspNet.AngularServices/npm/.npmignore b/Microsoft.AspNet.AngularServices/npm/.npmignore new file mode 100644 index 000000000000..9c3d4ebda8d8 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/.npmignore @@ -0,0 +1,3 @@ +/src/ +/tsconfig.json +/build.js diff --git a/Microsoft.AspNet.AngularServices/npm/build.js b/Microsoft.AspNet.AngularServices/npm/build.js new file mode 100644 index 000000000000..2a55704464fd --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/build.js @@ -0,0 +1,32 @@ +// ------------- +// No need to invoke this directly. To run a build, execute: +// npm run prepublish +// ------------- + +var Builder = require('systemjs-builder'); +var builder = new Builder('./'); +builder.config({ + defaultJSExtensions: true, + paths: { + 'angular2-aspnet': 'dist/Exports', + 'angular2-aspnet/*': 'dist/*' + }, + meta: { + 'angular2/*': { build: false } + } +}); + +var entryPoint = 'dist/Exports'; +var tasks = [ + builder.bundle(entryPoint, './bundles/angular2-aspnet.js'), + builder.bundle(entryPoint, './bundles/angular2-aspnet.min.js', { minify: true }) +]; + +Promise.all(tasks) + .then(function() { + console.log('Build complete'); + }) + .catch(function(err) { + console.error('Build error'); + console.error(err); + }); diff --git a/Microsoft.AspNet.AngularServices/npm/package.json b/Microsoft.AspNet.AngularServices/npm/package.json new file mode 100644 index 000000000000..870943222dd7 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/package.json @@ -0,0 +1,24 @@ +{ + "name": "angular2-aspnet", + "version": "0.0.1", + "description": "Helpers for Angular 2 apps built on ASP.NET", + "main": "./dist/Exports", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "prepublish": "tsc && node build.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/aspnet/NodeServices/tree/master/Microsoft.AspNet.AngularServices" + }, + "typings": "dist/Exports", + "author": "Microsoft", + "license": "Apache-2.0", + "peerDependencies": { + "angular2": "^2.0.0-alpha.44" + }, + "devDependencies": { + "systemjs-builder": "^0.14.11", + "typescript": "^1.7.3" + } +} diff --git a/Microsoft.AspNet.AngularServices/npm/src/Exports.ts b/Microsoft.AspNet.AngularServices/npm/src/Exports.ts new file mode 100644 index 000000000000..897995dd6eb1 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/src/Exports.ts @@ -0,0 +1 @@ +export * from './Validation'; diff --git a/Microsoft.AspNet.AngularServices/npm/src/Validation.ts b/Microsoft.AspNet.AngularServices/npm/src/Validation.ts new file mode 100644 index 000000000000..5ed2beb20066 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/src/Validation.ts @@ -0,0 +1,31 @@ +import { ControlGroup } from 'angular2/angular2'; +import { Response } from 'angular2/http'; + +export class Validation { + + public static showValidationErrors(response: ValidationErrorResult | Response, controlGroup: ControlGroup): void { + if (response instanceof Response) { + var httpResponse = response; + response = (httpResponse.json()); + } + + // It's not yet clear whether this is a legitimate and supported use of the ng.ControlGroup API. + // Need feedback from the Angular 2 team on whether there's a better way. + var errors = response; + Object.keys(errors || {}).forEach(key => { + errors[key].forEach(errorMessage => { + // This in particular is rough + if (!controlGroup.controls[key].errors) { + (controlGroup.controls[key])._errors = {}; + } + + controlGroup.controls[key].errors[errorMessage] = true; + }); + }); + } + +} + +export interface ValidationErrorResult { + [propertyName: string]: string[]; +} diff --git a/Microsoft.AspNet.AngularServices/npm/tsconfig.json b/Microsoft.AspNet.AngularServices/npm/tsconfig.json new file mode 100644 index 000000000000..c586e451b59d --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "sourceMap": false, + "declaration": true, + "noLib": false, + "outDir": "./dist" + }, + "exclude": [ + "node_modules" + ] +} From bf6548de96d8138da8e9b61351624cfde0268eb3 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 10 Dec 2015 14:24:38 +0000 Subject: [PATCH 0044/1585] Use angular2-aspnet in Angular 2 Music Store sample --- .../MusicStore/Views/Home/Index.cshtml | 1 + samples/angular/MusicStore/gulpfile.js | 1 + samples/angular/MusicStore/package.json | 1 + .../components/admin/album-edit/AspNetUtil.ts | 32 ------------------- .../components/admin/album-edit/album-edit.ts | 2 +- 5 files changed, 4 insertions(+), 33 deletions(-) delete mode 100644 samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts diff --git a/samples/angular/MusicStore/Views/Home/Index.cshtml b/samples/angular/MusicStore/Views/Home/Index.cshtml index acd6a30a3566..0fcf71297264 100755 --- a/samples/angular/MusicStore/Views/Home/Index.cshtml +++ b/samples/angular/MusicStore/Views/Home/Index.cshtml @@ -17,5 +17,6 @@ + } diff --git a/samples/angular/MusicStore/gulpfile.js b/samples/angular/MusicStore/gulpfile.js index 70895ec7f80d..187643294f10 100755 --- a/samples/angular/MusicStore/gulpfile.js +++ b/samples/angular/MusicStore/gulpfile.js @@ -25,6 +25,7 @@ var config = { require.resolve('angular2/bundles/angular2.dev.js'), require.resolve('angular2/bundles/router.dev.js'), require.resolve('angular2/bundles/http.dev.js'), + require.resolve('angular2-aspnet/bundles/angular2-aspnet.js'), require.resolve('jquery/dist/jquery.js'), require.resolve('bootstrap/dist/js/bootstrap.js') ] diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json index 251ee3396857..e664f3c7245c 100644 --- a/samples/angular/MusicStore/package.json +++ b/samples/angular/MusicStore/package.json @@ -3,6 +3,7 @@ "version": "0.0.0", "dependencies": { "angular2": "2.0.0-alpha.44", + "angular2-aspnet": "0.0.1", "angular2-universal-patched": "^0.5.4", "bootstrap": "^3.3.5", "es6-module-loader": "^0.15.0", diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts deleted file mode 100644 index c90b4063e9ab..000000000000 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/AspNetUtil.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { ControlGroup } from 'angular2/angular2'; -import { Response } from 'angular2/http'; - -// TODO: Factor this out into a separate NPM module -export class Validation { - - public static showValidationErrors(response: ValidationErrorResult | Response, controlGroup: ControlGroup): void { - if (response instanceof Response) { - var httpResponse = response; - response = (httpResponse.json()); - } - - // It's not yet clear whether this is a legitimate and supported use of the ng.ControlGroup API. - // Need feedback from the Angular 2 team on whether there's a better way. - var errors = response; - Object.keys(errors || {}).forEach(key => { - errors[key].forEach(errorMessage => { - // This in particular is rough - if (!controlGroup.controls[key].errors) { - (controlGroup.controls[key])._errors = {}; - } - - controlGroup.controls[key].errors[errorMessage] = true; - }); - }); - } - -} - -export interface ValidationErrorResult { - [propertyName: string]: string[]; -} diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index c7a943a86584..b61f53e53f6b 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -4,7 +4,7 @@ import * as models from '../../../models/models'; import { Http, HTTP_BINDINGS, Headers, Response } from 'angular2/http'; import { AlbumDeletePrompt } from '../album-delete-prompt/album-delete-prompt'; import { FormField } from '../form-field/form-field'; -import * as AspNet from './AspNetUtil'; +import * as AspNet from 'angular2-aspnet'; @ng.Component({ selector: 'album-edit' From c95b06d264983237ca213881ac37e57d913befe9 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 10 Dec 2015 14:56:15 +0000 Subject: [PATCH 0045/1585] Add some notes --- .../npm/readme.txt | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Microsoft.AspNet.AngularServices/npm/readme.txt diff --git a/Microsoft.AspNet.AngularServices/npm/readme.txt b/Microsoft.AspNet.AngularServices/npm/readme.txt new file mode 100644 index 000000000000..ed257e35f382 --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/readme.txt @@ -0,0 +1,59 @@ +If you just want to use this package, then you *don't have to build it*. Instead, just grab the prebuilt package from NPM: + + npm install angular2-aspnet + +The rest of this file is notes for anyone contributing to this package itself. + +## How to build + +Run the following: + + npm install + npm run prepublish + +Requirements: + + * Node, NPM + * `tsc` installed globally (via `npm install -g typescript`) + +## Project structure + +This package is intended to be consumable both on the server in Node.js, and on the client. Also, it's written in TypeScript, +which neither of those environments knows natively, but the TypeScript type definitions need to get delivered with the package +so that developers get a good IDE experience when consuming it. + +The build process is therefore: + +1. Compile the TypeScript to produce the development-time (.d.ts) and server-side (.js) artifacts + + `tsc` reads `tsconfig.json` and is instructed to compile all the `.ts` files in `src/`. It produces a corresponding + structure of `.js` and `.d.ts` files in `dist/`. + + When a developer consumes the resulting package (via `npm install angular2-aspnet`), + + - No additional copy of `angular2` will be installed, because this package's dependency on it is declared as a + `peerDependency`. This means it will work with whatever (compatible) version of `angular2` is already installed. + - At runtime inside Node.js, the `main` configuration in `package.json` means the developer can use a standard + `import` statement to consume this package (i.e., `import * from 'angular2-aspnet';` in either JS or TS files). + - At development time inside an IDE such as Visual Studio Code, the `typings` configuration in `package.json` means + the IDE will use the corresponding `.d.ts` file as type metadata for the variable imported that way. + +2. Use the SystemJS builder to produce the client-side artifacts + + `build.js` uses the SystemJS Builder API to combine files in `dist/` into `.js` files ready for use in client-side + SystemJS environments, and puts them in `bundles/`. The bundle files contain `System.register` calls so that any + other part of your client-side code that tries to import `angular2-aspnet` via SystemJS will get that module at runtime. + + To make it work in an application: + - Set up some build step that copies your chosen bundle file from `bundles/` to some location where it will + be served to the client + - Below your `", + JsonConvert.SerializeObject(url), + JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody }) + ); + } + } +} \ No newline at end of file diff --git a/Microsoft.AspNet.AngularServices/npm/build.js b/Microsoft.AspNet.AngularServices/npm/build.js index 2a55704464fd..f8fd31377633 100644 --- a/Microsoft.AspNet.AngularServices/npm/build.js +++ b/Microsoft.AspNet.AngularServices/npm/build.js @@ -12,7 +12,8 @@ builder.config({ 'angular2-aspnet/*': 'dist/*' }, meta: { - 'angular2/*': { build: false } + 'angular2/*': { build: false }, + '@reactivex/*': { build: false } } }); diff --git a/Microsoft.AspNet.AngularServices/npm/package.json b/Microsoft.AspNet.AngularServices/npm/package.json index 870943222dd7..ec35af7efe1d 100644 --- a/Microsoft.AspNet.AngularServices/npm/package.json +++ b/Microsoft.AspNet.AngularServices/npm/package.json @@ -1,6 +1,6 @@ { "name": "angular2-aspnet", - "version": "0.0.1", + "version": "0.0.3", "description": "Helpers for Angular 2 apps built on ASP.NET", "main": "./dist/Exports", "scripts": { @@ -15,7 +15,7 @@ "author": "Microsoft", "license": "Apache-2.0", "peerDependencies": { - "angular2": "^2.0.0-alpha.44" + "angular2": "2.0.0-alpha.44" }, "devDependencies": { "systemjs-builder": "^0.14.11", diff --git a/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts b/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts new file mode 100644 index 000000000000..e72f9620daaa --- /dev/null +++ b/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts @@ -0,0 +1,63 @@ +import { provide, Injectable, Provider } from 'angular2/core'; +import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethods, Response, ResponseOptions, ReadyStates } from 'angular2/http'; + +@Injectable() +export class CachePrimedConnectionBackend extends ConnectionBackend { + private _preCachedResponses: PreCachedResponses; + + constructor(private _underlyingBackend: ConnectionBackend, private _baseResponseOptions: ResponseOptions) { + super(); + this._preCachedResponses = (window).__preCachedResponses || {}; + } + + public createConnection(request: Request): Connection { + let cacheKey = request.url; + if (request.method === RequestMethods.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) { + return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions); + } else { + return this._underlyingBackend.createConnection(request); + } + } +} + +class CacheHitConnection implements Connection { + readyState: ReadyStates; + request: Request; + response: any; + + constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) { + this.request = req; + this.readyState = ReadyStates.Done; + + // Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic + // 'require', and not an 'import' statement, because the module isn't available on the server. + // All this badness goes away with the next update of Angular 2, as it exposes Observable directly from angular2/core. + // -- + // The current version of Angular exposes the following SystemJS module directly (it is *not* coming from the + // @reactivex/rxjs NPM package - it's coming from angular2). + let obsCtor: any = require('@reactivex/rxjs/dist/cjs/Observable'); + this.response = new obsCtor(responseObserver => { + let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode })); + responseObserver.next(response); + responseObserver.complete(); + }); + } +} + +declare var require: any; // Part of the workaround mentioned below. Can remove this after updating Angular. + +interface PreCachedResponses { + [url: string]: PreCachedResponse; +} + +interface PreCachedResponse { + statusCode: number; + body: string; +} + +export const CACHE_PRIMED_HTTP_PROVIDERS = [ + provide(Http, { + useFactory: (xhrBackend, requestOptions, responseOptions) => new Http(new CachePrimedConnectionBackend(xhrBackend, responseOptions), requestOptions), + deps: [XHRBackend, RequestOptions, ResponseOptions] + }), +]; diff --git a/Microsoft.AspNet.AngularServices/npm/src/Exports.ts b/Microsoft.AspNet.AngularServices/npm/src/Exports.ts index 897995dd6eb1..6ef8fb333e37 100644 --- a/Microsoft.AspNet.AngularServices/npm/src/Exports.ts +++ b/Microsoft.AspNet.AngularServices/npm/src/Exports.ts @@ -1 +1,2 @@ +export * from './CachePrimedHttp'; export * from './Validation'; diff --git a/Microsoft.AspNet.AngularServices/npm/tsconfig.json b/Microsoft.AspNet.AngularServices/npm/tsconfig.json index c586e451b59d..ecbeabdccbd3 100644 --- a/Microsoft.AspNet.AngularServices/npm/tsconfig.json +++ b/Microsoft.AspNet.AngularServices/npm/tsconfig.json @@ -4,6 +4,7 @@ "target": "es5", "sourceMap": false, "declaration": true, + "experimentalDecorators": true, "noLib": false, "outDir": "./dist" }, diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index 01fbeb691a2d..8fca29b377e1 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -19,6 +19,7 @@ "Microsoft.CSharp": "4.0.1-beta-*", "System.Collections": "4.0.11-beta-*", "System.Linq": "4.0.1-beta-*", + "System.Net.Http": "4.0.1-beta-*", "System.Runtime": "4.0.21-beta-*", "System.Threading": "4.0.11-beta-*" } From 8a0cbe789ec2ce6cb95231182f06a24bab19f070 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 10 Dec 2015 20:30:02 +0000 Subject: [PATCH 0049/1585] Use cache priming in Music Store sample --- samples/angular/MusicStore/Views/Home/Index.cshtml | 3 +++ samples/angular/MusicStore/Views/_ViewImports.cshtml | 1 + samples/angular/MusicStore/package.json | 2 +- .../MusicStore/wwwroot/ng-app/components/app/bootstrap.ts | 5 +++-- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/samples/angular/MusicStore/Views/Home/Index.cshtml b/samples/angular/MusicStore/Views/Home/Index.cshtml index 0fcf71297264..03074c6c1713 100755 --- a/samples/angular/MusicStore/Views/Home/Index.cshtml +++ b/samples/angular/MusicStore/Views/Home/Index.cshtml @@ -9,6 +9,9 @@ @section scripts { + @await Html.PrimeCache(Url.Action("GenreMenuList", "GenresApi")) + @await Html.PrimeCache(Url.Action("MostPopular", "AlbumsApi")) + diff --git a/samples/angular/MusicStore/Views/_ViewImports.cshtml b/samples/angular/MusicStore/Views/_ViewImports.cshtml index 24b10f41a501..7d8e7d619629 100755 --- a/samples/angular/MusicStore/Views/_ViewImports.cshtml +++ b/samples/angular/MusicStore/Views/_ViewImports.cshtml @@ -1,3 +1,4 @@ @using MusicStore +@using Microsoft.AspNet.AngularServices @addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" @addTagHelper "*, Microsoft.AspNet.AngularServices" diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json index e664f3c7245c..cccc2b67681b 100644 --- a/samples/angular/MusicStore/package.json +++ b/samples/angular/MusicStore/package.json @@ -3,7 +3,7 @@ "version": "0.0.0", "dependencies": { "angular2": "2.0.0-alpha.44", - "angular2-aspnet": "0.0.1", + "angular2-aspnet": "^0.0.3", "angular2-universal-patched": "^0.5.4", "bootstrap": "^3.3.5", "es6-module-loader": "^0.15.0", diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts index a7d6cf3955a8..46e9cd7ab650 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/app/bootstrap.ts @@ -1,6 +1,7 @@ import * as ng from 'angular2/angular2'; import * as router from 'angular2/router'; -import { Http, HTTP_BINDINGS } from 'angular2/http'; +import { Http, HTTP_PROVIDERS } from 'angular2/http'; +import { CACHE_PRIMED_HTTP_PROVIDERS } from 'angular2-aspnet'; import { App } from './app'; -ng.bootstrap(App, [router.ROUTER_BINDINGS, HTTP_BINDINGS, ng.FormBuilder]); +ng.bootstrap(App, [router.ROUTER_BINDINGS, HTTP_PROVIDERS, CACHE_PRIMED_HTTP_PROVIDERS, ng.FormBuilder]); From a83e4d85e445df17b243dd9b050a7eabc0eb25f3 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 11 Dec 2015 16:36:18 +0000 Subject: [PATCH 0050/1585] Show example of using traditional MVC controller+action routing alongside client-side routes --- samples/angular/MusicStore/Startup.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/samples/angular/MusicStore/Startup.cs b/samples/angular/MusicStore/Startup.cs index eda89585a4b1..59ee844015ce 100755 --- a/samples/angular/MusicStore/Startup.cs +++ b/samples/angular/MusicStore/Startup.cs @@ -110,12 +110,16 @@ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerF // Add MVC to the request pipeline. app.UseMvc(routes => { - // Matches any request that doesn't appear to have a filename extension (defined as 'having a dot in the last URI segment'). + // Matches requests that correspond to an existent controller/action pair + routes.MapRoute("default", "{controller}/{action}/{id:int?}"); + + // Matches any other request that doesn't appear to have a filename extension (defined as 'having a dot in the last URI segment'). // This means you'll correctly get 404s for /some/dir/non-existent-image.png instead of returning the SPA HTML. // However, it means requests like /customers/isaac.newton will *not* be mapped into the SPA, so if you need to accept // URIs like that you'll need to match all URIs, e.g.: - // routes.MapbackRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" }); - // (which of course will match /customers/isaac.png too - maybe that is a real customer name, not a PNG image). + // routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" }); + // (which of course will match /customers/isaac.png too, so in that case it would serve the PNG image at that URL if one is on disk, + // or the SPA HTML if not). routes.MapSpaFallbackRoute("spa-fallback", new { controller = "Home", action = "Index" }); // Uncomment the following line to add a route for porting Web API 2 controllers. From bba388944d9e66fd65720caa99773e430940f136 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Fri, 11 Dec 2015 19:06:30 +0000 Subject: [PATCH 0051/1585] Extremely minor improvement to sample --- .../wwwroot/ng-app/components/admin/album-edit/album-edit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index b61f53e53f6b..be2929ae1afb 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -69,7 +69,7 @@ export class AlbumEdit { var albumId = this.originalAlbum.AlbumId; this._putJson(`/api/albums/${ albumId }/update`, this.form.value).subscribe(response => { - if (response.status === 200) { + if (response.ok) { this.changesSaved = true; } else { AspNet.Validation.showValidationErrors(response, this.form); From 8a148d85357021d37fb0abdad817430e2b9166c1 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 14 Dec 2015 12:02:41 +0000 Subject: [PATCH 0052/1585] Move from ValidationErrorResult to HttpBadRequest, and support object-level errors too --- .../npm/src/Validation.ts | 13 ++++--- .../ValidationErrorResult.cs | 30 --------------- .../MusicStore/Apis/AlbumsApiController.cs | 23 +++++++++-- .../Apis/Models/SentimentAnalysis.cs | 38 +++++++++++++++++++ .../admin/album-edit/album-edit.html | 1 + .../components/admin/album-edit/album-edit.ts | 6 ++- 6 files changed, 72 insertions(+), 39 deletions(-) delete mode 100644 Microsoft.AspNet.SpaServices/ValidationErrorResult.cs create mode 100644 samples/angular/MusicStore/Apis/Models/SentimentAnalysis.cs diff --git a/Microsoft.AspNet.AngularServices/npm/src/Validation.ts b/Microsoft.AspNet.AngularServices/npm/src/Validation.ts index 5ed2beb20066..b6d7535be93a 100644 --- a/Microsoft.AspNet.AngularServices/npm/src/Validation.ts +++ b/Microsoft.AspNet.AngularServices/npm/src/Validation.ts @@ -14,16 +14,19 @@ export class Validation { var errors = response; Object.keys(errors || {}).forEach(key => { errors[key].forEach(errorMessage => { - // This in particular is rough - if (!controlGroup.controls[key].errors) { - (controlGroup.controls[key])._errors = {}; + // If there's a specific control for this key, then use it. Otherwise associate the error + // with the whole control group. + var control = controlGroup.controls[key] || controlGroup; + + // This is rough. Need to find out if there's a better way, or if this is even supported. + if (!control.errors) { + (control)._errors = {}; } - controlGroup.controls[key].errors[errorMessage] = true; + control.errors[errorMessage] = true; }); }); } - } export interface ValidationErrorResult { diff --git a/Microsoft.AspNet.SpaServices/ValidationErrorResult.cs b/Microsoft.AspNet.SpaServices/ValidationErrorResult.cs deleted file mode 100644 index 9cd0ba1d458d..000000000000 --- a/Microsoft.AspNet.SpaServices/ValidationErrorResult.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Microsoft.AspNet.Mvc; -using Microsoft.AspNet.Mvc.ModelBinding; - -namespace Microsoft.AspNet.SpaServices -{ - public class ValidationErrorResult : ObjectResult { - public const int DefaultStatusCode = 400; - - public ValidationErrorResult(ModelStateDictionary modelState, int errorStatusCode = DefaultStatusCode) - : base(CreateResultObject(modelState)) - { - if (!modelState.IsValid) { - this.StatusCode = errorStatusCode; - } - } - - private static IDictionary> CreateResultObject(ModelStateDictionary modelState) - { - if (modelState.IsValid) { - return null; - } else { - return modelState - .Where(m => m.Value.Errors.Any()) - .ToDictionary(m => m.Key, m => m.Value.Errors.Select(me => me.ErrorMessage)); - } - } - } -} diff --git a/samples/angular/MusicStore/Apis/AlbumsApiController.cs b/samples/angular/MusicStore/Apis/AlbumsApiController.cs index 3b57e0b5690d..55c4518403a3 100644 --- a/samples/angular/MusicStore/Apis/AlbumsApiController.cs +++ b/samples/angular/MusicStore/Apis/AlbumsApiController.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.ComponentModel.DataAnnotations; +using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNet.Authorization; using Microsoft.AspNet.Mvc; @@ -118,7 +120,7 @@ public async Task UpdateAlbum(int albumId, [FromBody] AlbumChangeD if (!ModelState.IsValid) { // Return the model errors - return new ValidationErrorResult(ModelState); + return HttpBadRequest(ModelState); } var dbAlbum = await _storeContext.Albums.SingleOrDefaultAsync(a => a.AlbumId == albumId); @@ -165,7 +167,7 @@ public async Task DeleteAlbum(int albumId) } [ModelMetadataType(typeof(Album))] - public class AlbumChangeDto + public class AlbumChangeDto : IValidatableObject { public int GenreId { get; set; } @@ -176,6 +178,21 @@ public class AlbumChangeDto public decimal Price { get; set; } public string AlbumArtUrl { get; set; } + + public IEnumerable Validate(ValidationContext validationContext) + { + // An example of object-level (i.e., multi-property) validation + if (this.GenreId == 13 /* Indie */) { + switch (SentimentAnalysis.GetSentiment(Title)) { + case SentimentAnalysis.SentimentResult.Positive: + yield return new ValidationResult("Sounds too positive. Indie music requires more ambiguity."); + break; + case SentimentAnalysis.SentimentResult.Negative: + yield return new ValidationResult("Sounds too negative. Indie music requires more ambiguity."); + break; + } + } + } } public class AlbumResultDto : AlbumChangeDto diff --git a/samples/angular/MusicStore/Apis/Models/SentimentAnalysis.cs b/samples/angular/MusicStore/Apis/Models/SentimentAnalysis.cs new file mode 100644 index 000000000000..e505d2ef4220 --- /dev/null +++ b/samples/angular/MusicStore/Apis/Models/SentimentAnalysis.cs @@ -0,0 +1,38 @@ +using System.Linq; +using System.Text.RegularExpressions; + +namespace MusicStore.Models +{ + // Obviously this is not a serious sentiment analyser. It is only here to provide an amusing demonstration of cross-property + // validation in AlbumsApiController. + public static class SentimentAnalysis + { + private static string[] positiveSentimentWords = new[] { "happy", "fun", "joy", "love", "delight", "bunny", "bunnies", "asp.net" }; + + private static string[] negativeSentimentWords = new[] { "sad", "pain", "despair", "hate", "scorn", "death", "package management" }; + + public static SentimentResult GetSentiment(string text) { + var numPositiveWords = CountWordOccurrences(text, positiveSentimentWords); + var numNegativeWords = CountWordOccurrences(text, negativeSentimentWords); + if (numPositiveWords > numNegativeWords) { + return SentimentResult.Positive; + } else if (numNegativeWords > numPositiveWords) { + return SentimentResult.Negative; + } else { + return SentimentResult.Neutral; + } + } + + private static int CountWordOccurrences(string text, string[] words) + { + // Very simplistic matching technique for this sample. Not scalable and not really even correct. + return new Regex(string.Join("|", words), RegexOptions.IgnoreCase).Matches(text).Count; + } + + public enum SentimentResult { + Negative, + Neutral, + Positive, + } + } +} \ No newline at end of file diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html index f32b27a535b9..5b26fbc65dcf 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.html @@ -37,6 +37,7 @@

Album Edit

Done! Your changes were saved.
+
{{ errorMessage }}
Back to List diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index be2929ae1afb..7f1b4e09cc3e 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -69,7 +69,7 @@ export class AlbumEdit { var albumId = this.originalAlbum.AlbumId; this._putJson(`/api/albums/${ albumId }/update`, this.form.value).subscribe(response => { - if (response.ok) { + if (response.status === 200) { this.changesSaved = true; } else { AspNet.Validation.showValidationErrors(response, this.form); @@ -88,6 +88,10 @@ export class AlbumEdit { headers: new Headers({ 'Content-Type': 'application/json' }) }); } + + public get formErrors() { + return Object.keys(this.form.errors || {}); + } } // TODO: Figure out what type declaration is provided by Angular/RxJs and use that instead From e9ba74761ddd803b527ba12c09b6645d974f9ae7 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 14 Dec 2015 12:43:56 +0000 Subject: [PATCH 0053/1585] Fixes --- samples/angular/MusicStore/Apis/AlbumsApiController.cs | 2 +- .../wwwroot/ng-app/components/admin/album-edit/album-edit.ts | 4 ++-- .../wwwroot/ng-app/components/admin/form-field/form-field.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/angular/MusicStore/Apis/AlbumsApiController.cs b/samples/angular/MusicStore/Apis/AlbumsApiController.cs index 55c4518403a3..4d066560bb18 100644 --- a/samples/angular/MusicStore/Apis/AlbumsApiController.cs +++ b/samples/angular/MusicStore/Apis/AlbumsApiController.cs @@ -98,7 +98,7 @@ public async Task CreateAlbum([FromBody]AlbumChangeDto album) if (!ModelState.IsValid) { // Return the model errors - return new ValidationErrorResult(ModelState); + return HttpBadRequest(ModelState); } // Save the changes to the DB diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts index 7f1b4e09cc3e..1c5834c4729a 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-edit/album-edit.ts @@ -89,8 +89,8 @@ export class AlbumEdit { }); } - public get formErrors() { - return Object.keys(this.form.errors || {}); + public get formErrors(): string[] { + return this.form.dirty ? Object.keys(this.form.errors || {}) : []; } } diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts index 7cf7a3007cb7..b7c2cd88169b 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/form-field/form-field.ts @@ -12,7 +12,7 @@ export class FormField { private validate: ng.AbstractControl; public get errorMessages() { - var errors = (this.validate && this.validate.touched && this.validate.errors) || {}; + var errors = (this.validate && this.validate.dirty && this.validate.errors) || {}; return Object.keys(errors).map(key => { return 'Error: ' + key; }); From 80575a092e8e3c26e21d6703e5c343548e652356 Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 14 Dec 2015 13:16:39 +0000 Subject: [PATCH 0054/1585] Don't ignore synchronous errors when calling Node --- .../Content/Node/angular-rendering.js | 26 +++++++++++-------- .../Content/Node/entrypoint-http.js | 6 ++++- .../HostingModels/HttpNodeInstance.cs | 4 +-- .../Content/Node/react-rendering.js | 14 ++++++---- 4 files changed, 31 insertions(+), 19 deletions(-) diff --git a/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js b/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js index 460400e7e30d..e9d5a29ed134 100644 --- a/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js +++ b/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js @@ -31,17 +31,21 @@ function findAngularComponent(options) { module.exports = { renderToString: function(callback, options) { - var component = findAngularComponent(options); - var serverBindings = [ - ngRouter.ROUTER_BINDINGS, - ngUniversal.HTTP_PROVIDERS, - ng.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }), - ngUniversal.SERVER_LOCATION_PROVIDERS - ]; + try { + var component = findAngularComponent(options); + var serverBindings = [ + ngRouter.ROUTER_BINDINGS, + ngUniversal.HTTP_PROVIDERS, + ng.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }), + ngUniversal.SERVER_LOCATION_PROVIDERS + ]; - return ngUniversal.renderToString(component, serverBindings).then( - function(successValue) { callback(null, successValue); }, - function(errorValue) { callback(errorValue); } - ); + return ngUniversal.renderToString(component, serverBindings).then( + function(successValue) { callback(null, successValue); }, + function(errorValue) { callback(errorValue); } + ); + } catch (synchronousException) { + callback(synchronousException); + } } }; diff --git a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js index 153d94f4b0f7..da812026fa45 100644 --- a/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js +++ b/Microsoft.AspNet.NodeServices/Content/Node/entrypoint-http.js @@ -39,7 +39,11 @@ var server = http.createServer(function(req, res) { } }; - func.apply(null, [callback].concat(bodyJson.args)); + try { + func.apply(null, [callback].concat(bodyJson.args)); + } catch (synchronousException) { + callback(synchronousException, null); + } }); }); diff --git a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs index d987df384f71..e19d74a5f4f9 100644 --- a/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs +++ b/Microsoft.AspNet.NodeServices/HostingModels/HttpNodeInstance.cs @@ -32,8 +32,8 @@ public override async Task Invoke(NodeInvocationInfo invocationInfo) { var response = await client.PostAsync("http://localhost:" + this._portNumber, payload); var responseString = await response.Content.ReadAsStringAsync(); - if (response.StatusCode != HttpStatusCode.OK) { - throw new Exception("Node module responded with error: " + responseString); + if (!response.IsSuccessStatusCode) { + throw new Exception("Call to Node module failed with error: " + responseString); } var responseIsJson = response.Content.Headers.ContentType.MediaType == "application/json"; diff --git a/Microsoft.AspNet.ReactServices/Content/Node/react-rendering.js b/Microsoft.AspNet.ReactServices/Content/Node/react-rendering.js index 74db4836089e..30aeb3d269e7 100644 --- a/Microsoft.AspNet.ReactServices/Content/Node/react-rendering.js +++ b/Microsoft.AspNet.ReactServices/Content/Node/react-rendering.js @@ -45,10 +45,14 @@ function loadViaBabel(module, filename) { module.exports = { renderToString: function(callback, options) { - var component = findReactComponent(options); - var history = createMemoryHistory(options.requestUrl); - var reactElement = React.createElement(component, { history: history }); - var html = ReactDOMServer.renderToString(reactElement); - callback(null, html); + try { + var component = findReactComponent(options); + var history = createMemoryHistory(options.requestUrl); + var reactElement = React.createElement(component, { history: history }); + var html = ReactDOMServer.renderToString(reactElement); + callback(null, html); + } catch (synchronousException) { + callback(synchronousException); + } } }; From d946c4b5b8ebf89c4bd0debe86a09f1f1b8a792e Mon Sep 17 00:00:00 2001 From: Henk Mollema Date: Mon, 14 Dec 2015 14:51:41 +0100 Subject: [PATCH 0055/1585] Address PR feedback --- .gitignore | 17 +---------------- Microsoft.AspNet.AngularServices/project.json | 3 ++- Microsoft.AspNet.NodeServices/project.json | 3 ++- Microsoft.AspNet.ReactServices/project.json | 3 ++- NuGet.config | 7 ------- 5 files changed, 7 insertions(+), 26 deletions(-) delete mode 100644 NuGet.config diff --git a/.gitignore b/.gitignore index f4e4990ec1bb..ec8cfe342914 100644 --- a/.gitignore +++ b/.gitignore @@ -1,31 +1,16 @@ [Oo]bj/ [Bb]in/ -TestResults/ .nuget/ -_ReSharper.*/ packages/ artifacts/ PublishProfiles/ *.user *.suo -*.cache -*.docstates -_ReSharper.* nuget.exe -*net45.csproj -*net451.csproj -*k10.csproj -*.psess -*.vsp -*.pidb *.userprefs *DS_Store -*.ncrunchsolution -*.*sdf -*.ipch -*.sln.ide node_modules *.sln.ide project.lock.json .vs/ -npm-debug.log \ No newline at end of file +npm-debug.log diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index f112b424b9ad..be078a00ba49 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -1,6 +1,7 @@ { "version": "1.0.0-alpha7", - "description": "ASP.NET 5 server-side Angular services.", + "description": "Helpers for building Angular 2 applications on ASP.NET 5.", + "authors": [ "Microsoft" ], "repository": { "type": "git", "url": "git://github.com/aspnet/nodeservices" diff --git a/Microsoft.AspNet.NodeServices/project.json b/Microsoft.AspNet.NodeServices/project.json index 9da3393fa897..16f59e0429de 100644 --- a/Microsoft.AspNet.NodeServices/project.json +++ b/Microsoft.AspNet.NodeServices/project.json @@ -1,6 +1,7 @@ { "version": "1.0.0-alpha7", - "description": "ASP.NET 5 Node.js services.", + "description": "Invoke Node.js modules at runtime in ASP.NET 5 applications.", + "authors": [ "Microsoft" ], "repository": { "type": "git", "url": "git://github.com/aspnet/nodeservices" diff --git a/Microsoft.AspNet.ReactServices/project.json b/Microsoft.AspNet.ReactServices/project.json index 418ab076881a..d4bf66554a36 100644 --- a/Microsoft.AspNet.ReactServices/project.json +++ b/Microsoft.AspNet.ReactServices/project.json @@ -1,6 +1,7 @@ { "version": "1.0.0-alpha7", - "description": "ASP.NET 5 server-side React services,", + "description": "Helpers for building React applications on ASP.NET 5.", + "authors": [ "Microsoft" ], "repository": { "type": "git", "url": "git://github.com/aspnet/nodeservices" diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index b004e5cc741b..000000000000 --- a/NuGet.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file From c56282a45216b2e2dc88bf104dcf06951122dffe Mon Sep 17 00:00:00 2001 From: Henk Mollema Date: Mon, 14 Dec 2015 14:54:32 +0100 Subject: [PATCH 0056/1585] More changes --- .gitignore | 6 ------ NodeServices.sln | 1 - 2 files changed, 7 deletions(-) diff --git a/.gitignore b/.gitignore index ec8cfe342914..0531f8ef0c7f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,10 @@ [Oo]bj/ [Bb]in/ -.nuget/ packages/ artifacts/ -PublishProfiles/ *.user *.suo -nuget.exe -*.userprefs -*DS_Store node_modules -*.sln.ide project.lock.json .vs/ npm-debug.log diff --git a/NodeServices.sln b/NodeServices.sln index 4da941efefbe..12f1f687576b 100644 --- a/NodeServices.sln +++ b/NodeServices.sln @@ -12,7 +12,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{78DAC76C-1092-45AB-BF0D-594B8C7B6569}" ProjectSection(SolutionItems) = preProject global.json = global.json - NuGet.config = NuGet.config EndProjectSection EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "MusicStore", "samples\angular\MusicStore\MusicStore.xproj", "{1A74148F-9DC0-435D-B5AC-7D1B0D3D5E0B}" From 2e0aed81380cdcb049896220f0d6e580a3e05f2e Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 14 Dec 2015 13:57:57 +0000 Subject: [PATCH 0057/1585] Slight simplification to MusicStore build process --- samples/angular/MusicStore/gulpfile.js | 3 +-- samples/angular/MusicStore/package.json | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/samples/angular/MusicStore/gulpfile.js b/samples/angular/MusicStore/gulpfile.js index 187643294f10..3719ede87247 100755 --- a/samples/angular/MusicStore/gulpfile.js +++ b/samples/angular/MusicStore/gulpfile.js @@ -5,7 +5,6 @@ var path = require('path'); var gulp = require('gulp'); var del = require('del'); -var eventStream = require('event-stream'); var typescript = require('gulp-typescript'); var inlineNg2Template = require('gulp-inline-ng2-template'); var sourcemaps = require('gulp-sourcemaps'); @@ -40,7 +39,7 @@ gulp.task('build', ['build.lib'], function () { var tsProject = typescript.createProject('./tsconfig.json', { typescript: require('typescript') }); var tsSrcInlined = gulp.src([webroot + '**/*.ts'], { base: webroot }) .pipe(inlineNg2Template({ base: webroot })); - return eventStream.merge(tsSrcInlined, gulp.src('Typings/**/*.ts')) + return tsSrcInlined .pipe(sourcemaps.init()) .pipe(typescript(tsProject)) .pipe(sourcemaps.write()) diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json index cccc2b67681b..c399ffaf63c7 100644 --- a/samples/angular/MusicStore/package.json +++ b/samples/angular/MusicStore/package.json @@ -16,7 +16,6 @@ }, "devDependencies": { "del": "^2.0.2", - "event-stream": "^3.3.1", "gulp": "^3.9.0", "gulp-inline-ng2-template": "0.0.7", "gulp-sourcemaps": "^1.6.0", From ef7e136f6eadafba78553f3d0ef98664e804df1b Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 14 Dec 2015 14:39:47 +0000 Subject: [PATCH 0058/1585] Remove unnecessary (and currently non-existent) namespace --- samples/angular/MusicStore/Apis/AlbumsApiController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/angular/MusicStore/Apis/AlbumsApiController.cs b/samples/angular/MusicStore/Apis/AlbumsApiController.cs index 4d066560bb18..fc28d6654e65 100644 --- a/samples/angular/MusicStore/Apis/AlbumsApiController.cs +++ b/samples/angular/MusicStore/Apis/AlbumsApiController.cs @@ -8,7 +8,6 @@ using AutoMapper; using MusicStore.Models; using MusicStore.Infrastructure; -using Microsoft.AspNet.SpaServices; namespace MusicStore.Apis { From 39f7f1649f433f49a2f2185c8e71c31500b871ff Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 15 Dec 2015 12:47:52 +0000 Subject: [PATCH 0059/1585] Add example of server and client validation for React --- .../Controllers/PeopleApiController.cs | 30 ++++++++++ .../ReactApp/components/PeopleGrid.jsx | 3 +- .../ReactApp/components/PersonEditor.jsx | 57 +++++++++++++++++++ .../ReactApp/components/ReactApp.jsx | 2 + .../data/{columnMeta.js => columnMeta.jsx} | 16 ++++++ .../react/ReactGrid/Views/Home/Index.cshtml | 2 +- .../ReactGrid/Views/Shared/_Layout.cshtml | 4 +- samples/react/ReactGrid/package.json | 2 + 8 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 samples/react/ReactGrid/Controllers/PeopleApiController.cs create mode 100644 samples/react/ReactGrid/ReactApp/components/PersonEditor.jsx rename samples/react/ReactGrid/ReactApp/data/{columnMeta.js => columnMeta.jsx} (66%) diff --git a/samples/react/ReactGrid/Controllers/PeopleApiController.cs b/samples/react/ReactGrid/Controllers/PeopleApiController.cs new file mode 100644 index 000000000000..82e967a6d8ba --- /dev/null +++ b/samples/react/ReactGrid/Controllers/PeopleApiController.cs @@ -0,0 +1,30 @@ +using System.ComponentModel.DataAnnotations; +using System.Threading.Tasks; +using Microsoft.AspNet.Mvc; + +namespace ReactExample.Controllers +{ + public class PeopleApiController : Controller + { + [HttpPut("api/people/{personId:int}")] + public async Task UpdatePerson([FromBody] PersonDto person) + { + if (!ModelState.IsValid) { + return HttpBadRequest(ModelState); + } else { + return new HttpOkResult(); + } + } + } + + public class PersonDto { + public string name { get; set; } + public string city { get; set; } + public string state { get; set; } + public string country { get; set; } + public string company { get; set; } + + [Range(1, 10)] + public int favoriteNumber { get; set; } + } +} diff --git a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx index c6662fae729b..a9dbec1c08d3 100644 --- a/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx +++ b/samples/react/ReactGrid/ReactApp/components/PeopleGrid.jsx @@ -2,7 +2,7 @@ import React from 'react'; import Griddle from 'griddle-react'; import { CustomPager } from './CustomPager.jsx'; import { fakeData } from '../data/fakeData.js'; -import { columnMeta } from '../data/columnMeta.js'; +import { columnMeta } from '../data/columnMeta.jsx'; const resultsPerPage = 10; export class PeopleGrid extends React.Component { @@ -13,6 +13,7 @@ export class PeopleGrid extends React.Component {

People

x.columnName)} columnMetadata={columnMeta} resultsPerPage={resultsPerPage} tableClassName="table" diff --git a/samples/react/ReactGrid/ReactApp/components/PersonEditor.jsx b/samples/react/ReactGrid/ReactApp/components/PersonEditor.jsx new file mode 100644 index 000000000000..6cb11d5c6291 --- /dev/null +++ b/samples/react/ReactGrid/ReactApp/components/PersonEditor.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import Formsy from 'formsy-react'; +import { Input } from 'formsy-react-components'; +import { fakeData } from '../data/fakeData.js'; + +export class PersonEditor extends React.Component { + constructor() { + super(); + this.state = { savedChanges: false }; + } + + onChange() { + this.setState({ savedChanges: false }); + } + + submit(model, reset, setErrors) { + PersonEditor.sendJson('put', `/api/people/${ this.props.params.personId }`, model).then(response => { + if (response.ok) { + this.setState({ savedChanges: true }); + } else { + // Parse server-side validation errors from the response and display them + response.json().then(setErrors); + } + }); + } + + render() { + var personId = parseInt(this.props.params.personId); + var person = fakeData.filter(p => p.id === personId)[0]; + var notificationBox = this.state.savedChanges + &&
Done! Your changes were saved.
; + + return
+
+

Edit { person.name }

+
+ + + + + + + + { notificationBox } + + +
; + } + + static sendJson(method, url, object) { + return fetch(url, { + method: method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(object) + }); + } +} diff --git a/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx b/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx index f135152bb560..1efeef7d4324 100644 --- a/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx +++ b/samples/react/ReactGrid/ReactApp/components/ReactApp.jsx @@ -1,6 +1,7 @@ import React from 'react'; import { Router, Route } from 'react-router'; import { PeopleGrid } from './PeopleGrid.jsx'; +import { PersonEditor } from './PersonEditor.jsx'; export default class ReactApp extends React.Component { render() { @@ -8,6 +9,7 @@ export default class ReactApp extends React.Component { + ); } diff --git a/samples/react/ReactGrid/ReactApp/data/columnMeta.js b/samples/react/ReactGrid/ReactApp/data/columnMeta.jsx similarity index 66% rename from samples/react/ReactGrid/ReactApp/data/columnMeta.js rename to samples/react/ReactGrid/ReactApp/data/columnMeta.jsx index f470451a68dd..ba23d4f4b6d6 100644 --- a/samples/react/ReactGrid/ReactApp/data/columnMeta.js +++ b/samples/react/ReactGrid/ReactApp/data/columnMeta.jsx @@ -1,3 +1,12 @@ +import React from 'react'; +import { Link } from 'react-router'; + +class RowActionsComponent extends React.Component { + render() { + return Edit; + } +} + var columnMeta = [ { "columnName": "id", @@ -40,6 +49,13 @@ var columnMeta = [ "order": 7, "locked": false, "visible": true + }, + { + "columnName": "actions", + "order": 8, + "locked": true, + "visible": true, + "customComponent": RowActionsComponent } ]; diff --git a/samples/react/ReactGrid/Views/Home/Index.cshtml b/samples/react/ReactGrid/Views/Home/Index.cshtml index 76f8a6c5fb78..25613ebb0c5a 100755 --- a/samples/react/ReactGrid/Views/Home/Index.cshtml +++ b/samples/react/ReactGrid/Views/Home/Index.cshtml @@ -1,5 +1,5 @@
@section scripts { - + } diff --git a/samples/react/ReactGrid/Views/Shared/_Layout.cshtml b/samples/react/ReactGrid/Views/Shared/_Layout.cshtml index 4e83db4cc892..a6fd7d57f8ac 100755 --- a/samples/react/ReactGrid/Views/Shared/_Layout.cshtml +++ b/samples/react/ReactGrid/Views/Shared/_Layout.cshtml @@ -6,7 +6,9 @@ - @RenderBody() +
+ @RenderBody() +
@RenderSection("scripts", required: false) diff --git a/samples/react/ReactGrid/package.json b/samples/react/ReactGrid/package.json index 2249b9171364..0d92af9c51cf 100644 --- a/samples/react/ReactGrid/package.json +++ b/samples/react/ReactGrid/package.json @@ -4,6 +4,8 @@ "dependencies": { "babel-core": "^5.8.29", "bootstrap": "^3.3.5", + "formsy-react": "^0.17.0", + "formsy-react-components": "^0.6.3", "griddle-react": "^0.2.14", "history": "^1.12.6", "react": "^0.14.0", From 269b31469c0c6ec4382be470dd52f565b5e868db Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Tue, 15 Dec 2015 15:15:16 +0000 Subject: [PATCH 0060/1585] Slight simplification --- .../angular/MusicStore/Controllers/HomeController.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/samples/angular/MusicStore/Controllers/HomeController.cs b/samples/angular/MusicStore/Controllers/HomeController.cs index aabaed6bf4e5..cd156631adc5 100755 --- a/samples/angular/MusicStore/Controllers/HomeController.cs +++ b/samples/angular/MusicStore/Controllers/HomeController.cs @@ -1,8 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.AspNet.Http; using Microsoft.AspNet.Mvc; namespace MusicStore.Controllers @@ -11,12 +6,7 @@ public class HomeController : Controller { public IActionResult Index() { - var url = Request.Path.Value; - if (url.EndsWith(".ico") || url.EndsWith(".map")) { - return new HttpStatusCodeResult(404); - } else { - return View(); - } + return View(); } public IActionResult Error() From 79c39d0f6c7dd87e3dcfde92e9301230a7478cdb Mon Sep 17 00:00:00 2001 From: Elrashid Date: Fri, 25 Dec 2015 02:19:12 +0400 Subject: [PATCH 0061/1585] change Range Attribute typeof double to decimal to match Price type --- samples/angular/MusicStore/Apis/Models/Album.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/angular/MusicStore/Apis/Models/Album.cs b/samples/angular/MusicStore/Apis/Models/Album.cs index 919bf1b1f74e..e7d9246f9d12 100644 --- a/samples/angular/MusicStore/Apis/Models/Album.cs +++ b/samples/angular/MusicStore/Apis/Models/Album.cs @@ -23,7 +23,7 @@ public Album() public string Title { get; set; } [Required] - [RangeAttribute(typeof(double), "0.01", "100")] // Long-form constructor to work around https://github.com/dotnet/coreclr/issues/2172 + [RangeAttribute(typeof(decimal), "0.01", "100")] // Long-form constructor to work around https://github.com/dotnet/coreclr/issues/2172 [DataType(DataType.Currency)] public decimal Price { get; set; } @@ -37,4 +37,4 @@ public Album() public virtual ICollection OrderDetails { get; set; } } -} \ No newline at end of file +} From f44b84f2ab11c0a429682d57b8b3b9df945b76ff Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Thu, 7 Jan 2016 11:51:55 +0000 Subject: [PATCH 0062/1585] Bump SpaServices version to alpha7-1 to avoid conflict on NuGet --- Microsoft.AspNet.AngularServices/project.json | 2 +- Microsoft.AspNet.ReactServices/project.json | 2 +- Microsoft.AspNet.SpaServices/project.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Microsoft.AspNet.AngularServices/project.json b/Microsoft.AspNet.AngularServices/project.json index 7ae3accdd3c3..f828c3c8575d 100644 --- a/Microsoft.AspNet.AngularServices/project.json +++ b/Microsoft.AspNet.AngularServices/project.json @@ -25,7 +25,7 @@ "dependencies": { "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final", "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", - "Microsoft.AspNet.SpaServices": "1.0.0-alpha7" + "Microsoft.AspNet.SpaServices": "1.0.0-alpha7-1" }, "resource": [ "Content/**/*" diff --git a/Microsoft.AspNet.ReactServices/project.json b/Microsoft.AspNet.ReactServices/project.json index d4bf66554a36..e12d64b0d5dc 100644 --- a/Microsoft.AspNet.ReactServices/project.json +++ b/Microsoft.AspNet.ReactServices/project.json @@ -12,7 +12,7 @@ "dependencies": { "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-rc1-final", "Microsoft.AspNet.NodeServices": "1.0.0-alpha7", - "Microsoft.AspNet.SpaServices": "1.0.0-alpha7" + "Microsoft.AspNet.SpaServices": "1.0.0-alpha7-1" }, "frameworks": { "net451": { }, diff --git a/Microsoft.AspNet.SpaServices/project.json b/Microsoft.AspNet.SpaServices/project.json index 0edec8e6f9c6..e53886bfeb46 100644 --- a/Microsoft.AspNet.SpaServices/project.json +++ b/Microsoft.AspNet.SpaServices/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-alpha7", + "version": "1.0.0-alpha7-1", "description": "Microsoft.AspNet.SpaServices", "authors": [ "Microsoft" From 381b7b884ee2e25025e9dd9f23e234884f5fdcbf Mon Sep 17 00:00:00 2001 From: SteveSandersonMS Date: Mon, 25 Jan 2016 15:13:30 +0000 Subject: [PATCH 0063/1585] Update to Angular 2 Beta 1. New bug: no longer waits for server-side HTTP requests to complete - waiting for info to resolve this. --- .../Content/Node/angular-rendering.js | 10 +++--- Microsoft.AspNet.AngularServices/npm/build.js | 2 +- .../npm/package.json | 3 +- .../npm/src/CachePrimedHttp.ts | 14 +++----- .../npm/src/Validation.ts | 2 +- .../MusicStore/Views/Home/Index.cshtml | 3 +- samples/angular/MusicStore/gulpfile.js | 5 +-- samples/angular/MusicStore/package.json | 5 +-- .../components/admin/admin-home/admin-home.ts | 8 ++--- .../album-delete-prompt.html | 2 +- .../album-delete-prompt.ts | 5 +-- .../admin/album-details/album-details.html | 6 ++-- .../admin/album-details/album-details.ts | 5 +-- .../admin/album-edit/album-edit.html | 12 +++---- .../components/admin/album-edit/album-edit.ts | 35 ++++++++++--------- .../admin/albums-list/albums-list.html | 10 +++--- .../admin/albums-list/albums-list.ts | 5 +-- .../admin/form-field/form-field.html | 4 +-- .../components/admin/form-field/form-field.ts | 7 ++-- .../wwwroot/ng-app/components/app/app.html | 12 +++---- .../wwwroot/ng-app/components/app/app.ts | 15 ++++---- .../ng-app/components/app/bootstrap.ts | 5 +-- .../public/album-details/album-details.html | 2 +- .../public/album-details/album-details.ts | 5 +-- .../public/album-tile/album-tile.html | 2 +- .../public/album-tile/album-tile.ts | 4 +-- .../public/genre-contents/genre-contents.html | 2 +- .../public/genre-contents/genre-contents.ts | 5 +-- .../public/genres-list/genres-list.html | 6 ++-- .../public/genres-list/genres-list.ts | 5 +-- .../ng-app/components/public/home/home.html | 2 +- .../ng-app/components/public/home/home.ts | 5 +-- 32 files changed, 113 insertions(+), 100 deletions(-) diff --git a/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js b/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js index e9d5a29ed134..db75eb346edd 100644 --- a/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js +++ b/Microsoft.AspNet.AngularServices/Content/Node/angular-rendering.js @@ -1,6 +1,7 @@ var path = require('path'); -var ngUniversal = require('angular2-universal-patched'); -var ng = require('angular2/angular2'); +var ngUniversal = require('angular2-universal-preview'); +var ngUniversalRender = require('angular2-universal-preview/dist/server/src/render'); +var ngCore = require('angular2/core'); var ngRouter = require('angular2/router'); function getExportOrThrow(moduleInstance, moduleFilename, exportName) { @@ -36,11 +37,12 @@ module.exports = { var serverBindings = [ ngRouter.ROUTER_BINDINGS, ngUniversal.HTTP_PROVIDERS, - ng.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }), + ngCore.provide(ngUniversal.BASE_URL, { useValue: options.requestUrl }), + ngCore.provide(ngRouter.APP_BASE_HREF, { useValue: '/' }), ngUniversal.SERVER_LOCATION_PROVIDERS ]; - return ngUniversal.renderToString(component, serverBindings).then( + return ngUniversalRender.renderToString(component, serverBindings).then( function(successValue) { callback(null, successValue); }, function(errorValue) { callback(errorValue); } ); diff --git a/Microsoft.AspNet.AngularServices/npm/build.js b/Microsoft.AspNet.AngularServices/npm/build.js index f8fd31377633..937708a4336f 100644 --- a/Microsoft.AspNet.AngularServices/npm/build.js +++ b/Microsoft.AspNet.AngularServices/npm/build.js @@ -13,7 +13,7 @@ builder.config({ }, meta: { 'angular2/*': { build: false }, - '@reactivex/*': { build: false } + 'rxjs/*': { build: false } } }); diff --git a/Microsoft.AspNet.AngularServices/npm/package.json b/Microsoft.AspNet.AngularServices/npm/package.json index ec35af7efe1d..2262070c4ae3 100644 --- a/Microsoft.AspNet.AngularServices/npm/package.json +++ b/Microsoft.AspNet.AngularServices/npm/package.json @@ -15,7 +15,8 @@ "author": "Microsoft", "license": "Apache-2.0", "peerDependencies": { - "angular2": "2.0.0-alpha.44" + "angular2": "2.0.0-beta.1", + "rxjs": "5.0.0-beta.0" }, "devDependencies": { "systemjs-builder": "^0.14.11", diff --git a/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts b/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts index e72f9620daaa..991dd243b5c4 100644 --- a/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts +++ b/Microsoft.AspNet.AngularServices/npm/src/CachePrimedHttp.ts @@ -1,5 +1,5 @@ import { provide, Injectable, Provider } from 'angular2/core'; -import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethods, Response, ResponseOptions, ReadyStates } from 'angular2/http'; +import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethod, Response, ResponseOptions, ReadyState } from 'angular2/http'; @Injectable() export class CachePrimedConnectionBackend extends ConnectionBackend { @@ -12,7 +12,7 @@ export class CachePrimedConnectionBackend extends ConnectionBackend { public createConnection(request: Request): Connection { let cacheKey = request.url; - if (request.method === RequestMethods.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) { + if (request.method === RequestMethod.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) { return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions); } else { return this._underlyingBackend.createConnection(request); @@ -21,21 +21,17 @@ export class CachePrimedConnectionBackend extends ConnectionBackend { } class CacheHitConnection implements Connection { - readyState: ReadyStates; + readyState: ReadyState; request: Request; response: any; constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) { this.request = req; - this.readyState = ReadyStates.Done; + this.readyState = ReadyState.Done; // Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic // 'require', and not an 'import' statement, because the module isn't available on the server. - // All this badness goes away with the next update of Angular 2, as it exposes Observable directly from angular2/core. - // -- - // The current version of Angular exposes the following SystemJS module directly (it is *not* coming from the - // @reactivex/rxjs NPM package - it's coming from angular2). - let obsCtor: any = require('@reactivex/rxjs/dist/cjs/Observable'); + let obsCtor: any = require('rxjs/Observable').Observable; this.response = new obsCtor(responseObserver => { let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode })); responseObserver.next(response); diff --git a/Microsoft.AspNet.AngularServices/npm/src/Validation.ts b/Microsoft.AspNet.AngularServices/npm/src/Validation.ts index b6d7535be93a..0745228cd84c 100644 --- a/Microsoft.AspNet.AngularServices/npm/src/Validation.ts +++ b/Microsoft.AspNet.AngularServices/npm/src/Validation.ts @@ -1,4 +1,4 @@ -import { ControlGroup } from 'angular2/angular2'; +import { ControlGroup } from 'angular2/common'; import { Response } from 'angular2/http'; export class Validation { diff --git a/samples/angular/MusicStore/Views/Home/Index.cshtml b/samples/angular/MusicStore/Views/Home/Index.cshtml index 03074c6c1713..a5d33b4ef3bc 100755 --- a/samples/angular/MusicStore/Views/Home/Index.cshtml +++ b/samples/angular/MusicStore/Views/Home/Index.cshtml @@ -12,11 +12,12 @@ @await Html.PrimeCache(Url.Action("GenreMenuList", "GenresApi")) @await Html.PrimeCache(Url.Action("MostPopular", "AlbumsApi")) + - + diff --git a/samples/angular/MusicStore/gulpfile.js b/samples/angular/MusicStore/gulpfile.js index 3719ede87247..e244fc5c206b 100755 --- a/samples/angular/MusicStore/gulpfile.js +++ b/samples/angular/MusicStore/gulpfile.js @@ -17,16 +17,17 @@ var config = { lib: [ require.resolve('bootstrap/dist/css/bootstrap.css'), path.dirname(require.resolve('bootstrap/dist/fonts/glyphicons-halflings-regular.woff')) + '/**', + require.resolve('angular2/bundles/angular2-polyfills.js'), require.resolve('traceur/bin/traceur-runtime.js'), require.resolve('es6-module-loader/dist/es6-module-loader-sans-promises.js'), - require.resolve('reflect-metadata/Reflect.js'), require.resolve('systemjs/dist/system.src.js'), require.resolve('angular2/bundles/angular2.dev.js'), require.resolve('angular2/bundles/router.dev.js'), require.resolve('angular2/bundles/http.dev.js'), require.resolve('angular2-aspnet/bundles/angular2-aspnet.js'), require.resolve('jquery/dist/jquery.js'), - require.resolve('bootstrap/dist/js/bootstrap.js') + require.resolve('bootstrap/dist/js/bootstrap.js'), + require.resolve('rxjs/bundles/Rx.js') ] }; diff --git a/samples/angular/MusicStore/package.json b/samples/angular/MusicStore/package.json index c399ffaf63c7..6bc646f29076 100644 --- a/samples/angular/MusicStore/package.json +++ b/samples/angular/MusicStore/package.json @@ -2,15 +2,16 @@ "name": "MusicStore", "version": "0.0.0", "dependencies": { - "angular2": "2.0.0-alpha.44", + "angular2": "2.0.0-beta.1", "angular2-aspnet": "^0.0.3", - "angular2-universal-patched": "^0.5.4", + "angular2-universal-preview": "^0.32.2", "bootstrap": "^3.3.5", "es6-module-loader": "^0.15.0", "jquery": "^2.1.4", "less": "^2.5.3", "lodash": "^3.10.1", "reflect-metadata": "^0.1.2", + "rxjs": "^5.0.0-beta.0", "systemjs": "^0.19.3", "traceur": "0.0.91" }, diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts index 73eeafc4d765..8bcb893cc186 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/admin-home/admin-home.ts @@ -1,4 +1,4 @@ -import * as ng from 'angular2/angular2'; +import * as ng from 'angular2/core'; import * as router from 'angular2/router'; import { AlbumsList } from '../albums-list/albums-list'; import { AlbumDetails } from '../album-details/album-details'; @@ -8,9 +8,9 @@ import { AlbumEdit } from '../album-edit/album-edit'; selector: 'admin-home' }) @router.RouteConfig([ - { path: 'albums', as: 'Albums', component: AlbumsList }, - { path: 'album/details/:albumId', as: 'AlbumDetails', component: AlbumDetails }, - { path: 'album/edit/:albumId', as: 'AlbumEdit', component: AlbumEdit } + { path: 'albums', name: 'Albums', component: AlbumsList }, + { path: 'album/details/:albumId', name: 'AlbumDetails', component: AlbumDetails }, + { path: 'album/edit/:albumId', name: 'AlbumEdit', component: AlbumEdit } ]) @ng.View({ templateUrl: './ng-app/components/admin/admin-home/admin-home.html', diff --git a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html index a04250c7eee4..2f6b0ba63d3d 100644 --- a/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html +++ b/samples/angular/MusicStore/wwwroot/ng-app/components/admin/album-delete-prompt/album-delete-prompt.html @@ -1,5 +1,5 @@