This series continues from the app that is built in the Introduction to ASP.NET Core course, starting at the Creating a Form section.
Introduction to Tag Helpers
Authentication
Custom Middleware
Dependency Injection
APIs and MVC Core
SPAs and Angular2
Entity Framework Core
Publishing and Deployment
Tag Helpers
Enable Third-Party Authentication
Facebook Authentication
Dependency Injection Details
Node.js
Yeoman
Webpack
Angular2
Visual Studio Dev Essentials
Azure Portal
Visual Studio Code - Version Control
GitHub
<a asp-action="Create">Create New</a>
Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files.
The above tag-helper asp-action="Create"
renders as:
<a href="/movies/Create">Create New</a>
In the ASP.NET Core web app template, tag helpers are added via the Views/_ViewImports.cshtml
page:
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
_ViewImports.cshtml
allows you to specify references to dependencies that should be available in all views.
This provides tag helpers for the following:
- Cache
- Environment
- ValidationMessage
- ValidationSummary
- Anchor
- Form
- Input
- Label
- Link
- Option
- Script
- Select
- TextArea
Tag helpers don't have to necessarily augment an existing HTML tag.
Tag helpers are reference in Views/_ViewImports.cshtml
. The above mentioned tag helpers are contained in the Microsoft.AspNetCore.Mvc.TagHelpers
library.
_ViewImports.cshtml
@using mvc_form
@using mvc_form.Models
@using mvc_form.Models.AccountViewModels
@using mvc_form.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
Views/Movies/Index.cshtml
<-- Heading Details -->
<h2>Index</h2>
<environment names="Staging,Production">
<cache expires-after="@TimeSpan.FromSeconds(10)">
@DateTime.Now.ToString("dd MMM yyyy HH:mm:ss")
</cache>
</environment>
<-- Remaining Markup -->
In the above markup, if the current environment is staging or production, the server will cache the DateTime
for 10 seconds. Any subsequent navigation within 10 seconds will render the cached result.
Create RepeatTagHelper.cs
in project root:
RepeatTagHelper.cs
using Microsoft.AspNetCore.Razor.TagHelpers;
using System.Threading.Tasks;
namespace mvc_form
{
/// <summary>
/// <repeat count-of-things="5">HTML</repeat>
/// </summary>
public class RepeatTagHelper : TagHelper
{
public int CountOfThings { get; set; }
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
for (var i = 0; i < CountOfThings; i++)
{
output.Content.AppendHtml(await output.GetChildContentAsync(useCachedResult: false));
}
}
}
}
Add tag helper reference in _ViewImports.cshtml
:
@using mvc_form
@using mvc_form.Models
@using mvc_form.Models.AccountViewModels
@using mvc_form.Models.ManageViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, mvc-form
Given the admittedly strange naming convention I've used for this project, the
@addTagHelper *, mvc-form
declaration differs from the using statements because the .dll generated actually uses themvc-form
name rather than themvc_form
namespace that Visual Studio automatically changed to when the project was created. See this StackOverflow Answer
Usage in Views/Movies/Index.cshtml
:
<-- Heading Details -->
<h2>Index</h2>
<repeat count-of-things="5">
<p>Repeated using custom tag helper</p>
</repeat>
<-- Remaining Markup -->
This will repeat the paragraph 5 times in the rendered HTML.
- Create new ASP.NET Core Web App Project
- Select Web Application template, then click Change Authentication
- Select Individual Authentication from the authentication options, then create the project
- Controllers
AccountController.cs
ManageController.cs
- Data
- Migrations
xxx_CreateIdentitySchema.cs
ApplicationDbContextModelSnapshot.cs
ApplicationDbContext.cs
- Migrations
- Models
- AccountViewModels
ExternalLoginConfirmationViewModel.cs
ForgotPasswordViewModel.cs
LoginViewModel.cs
RegisterViewModel.cs
ResetPassowrdViewModel.cs
SendCodeViewModel.cs
VerifyCodeViewModel.cs
- ManageViewModels
AddPhoneNumberViewModel.cs
ChangePasswordViewModel.cs
ConfigureTwoFactorViewModel.cs
FactorViewModel.cs
IndexViewModel.cs
ManageLoginsViewModel.cs
RemoveLoginViewModel.cs
SetPasswordViewModel.cs
VerifyPhoneNumberViewModel.cs
ApplicationUser.cs
- AccountViewModels
- Services
IEmailSender.cs
ISmsSender.cs
MessageServices.cs
- Views
- Account
ConfirmEmail.cshtml
ExternalLoginConfirmation.cshtml
ExternalLoginFailure.cshtml
ForgotPassword.cshtml
ForgotPasswordConfirmation.cshtml
Lockout.cshtml
Login.cshtml
Register.cshtml
ResetPassword.cshtml
ResetPasswordConfirmation.cshtml
SendCode.cshtml
VerifyCode.cshtml
- Manage
AddPhoneNumber.cshtml
ChangePassword.cshtml
Index.cshtml
ManageLogins.cshtml
SetPassword.cshtml
VerifyPhoneNumber.cshtml
- Account
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProvider();
services.AddMvc();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Additional Configuration
app.UseIdentity();
// AdditionalConfiguration
}
- Right-click the project, and click Properties, then click the Debug tag. From here, check the Enable SSL option at the bottom.
- In
launchSettings.json
, configure theIIS Express
profile'slaunchUrl
attribute to make use of the URL pointing to thesslPort
value specified iniisSettings
:
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:51394/",
"sslPort": 44339
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "https://localhost:44339",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
- Trust the IIS Express-generated self-signed SSL Certificate
- Install the self-signed SSL Certificate
Execute
- Execute the application, and click the Register link at the right side of the navbar at the top
- Fill in the account details, then click Register. You will be met with a database error page specifying that the database cannot be opened because there are pending migrations against the database context.
- Click the Apply Migrations button to execute the migrations
- Once the migration has completed, refresh the page and notice that a user account has been authenticated
- Execute the web app, then click the Login link on the right of the navbar at the top of the page. Note the Use another service to login. section to the right of the page. Clicking the this article link will launch instructions for how to configure third-party authentication. This demonstration will focus on enabling Facebook Authentication.
- Navigate to https://developers.facebook.com/apps, sign in, then click Create a New App
- Fill in the details of the Create a New App ID form, then click Create App ID
- The App ID is generated, and authentication now needs to be configured. In the Product Setup region, click Get Started under Facebook Login
- In the Valid OAuth redirect URIs box, enter https://localhost:{ssl-port-number}/signin-facebook, then click Save Changes at the bottom of the page
- Navigate to the Dashboard using the link on the left side of the page. Click Show next to App Secret. The App ID and App Secret values need to be stored in the user-secrets configuration section of the application so that authentication can be configured.
- In visual studio, right-click the project and click Open Folder in File Explorer. In file explorer, hold shift and right-click in an empty area of the folder, then click Open Command Window Here. Register the App ID and App Secret using the
dotnet
command.
dotnet user-secrets set Authentication:Facebook:AppId {app-id}
dotnet user-secrets set Authentication:Facebook:AppSecret {app-secret}
- In
project.json
, registerMicrosoft.AspNetCore.Authentication.Facebook
as a dependency
{
"dependencies": {
"Microsoft.AspNetCore.Authentication.Facebook": "1.0.0"
}
}
- Configure facebook authentication in
Startup.cs
Startup.cs
public Startup(IHostingEnvironment)
{
// Additional Constructor logic
if (env.IsDevelopment())
{
builder.AddUserSecrets();
}
// Additional Constructor logic
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Additional Configuration
// Add external authentication middleware below. To configure them please see http://go.microsoft.com/fwlink/?LinkID=532715
app.UseFacebookAuthentication(new FacebookOptions()
{
AppId = Configuration["Authentication:Facebook:AppId"],
AppSecret = Configuration["Authentication:Facebook:AppSecret"]
});
// Additional Configuration
}
-
Execute the app and navigate to the Login page. Click the Facebook button under the Use another service to log in. section
-
Click Continue as {Your Name Here}
- Enter an email to associate the account with your account on the web app, then click Register
- New ASP.NET Core Web App Project
- Empty Web Template
- Add
project.json
dependencies
project.json
{
"dependencies": {
"Microsoft.Extensions.Configuration.FileExtensions": "1.1.0",
"Microsoft.Extensions.Configuration.Json": "1.0.0",
"Microsoft.AspNetCore.Mvc": "1.1.0"
}
}
- Configure
Startup.cs
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
namespace middleware
{
public class Startup
{
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
-
Create the following directory structure in the project root:
-
Controllers
HomeController.cs
-
Models
- Home
IndexModel.cs
- Home
-
Views
- Home
Index.cshtml
- Home
IndexModel.cs
namespace middleware.Models.Home
{
public class IndexModel
{
public string Name { get; set; }
public int Age { get; set; }
}
}
HomeController.cs
using Microsoft.AspNetCore.Mvc;
using middleware.Models.Home;
namespace middleware.Controllers
{
public class HomeController : Controller
{
public IActionResult Index()
{
var model = new IndexModel
{
Name = "Jaime",
Age = 31
};
return View(model);
}
}
}
Index.cshtml
@model middleware.Models.Home.IndexModel
<h1>Person!</h1>
<p>Name: @Model.Name</p>
<p>Age: @Model.Age</p>
This demonstrates how to manually generate an HTTP response when an exception is caught in an environment other than development when an exception is caught.
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(subApp =>
{
subApp.Run(async context =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<h2>All the Bad Things Happened!</h2>");
await context.Response.WriteAsync("<p>Application Error in Production. Contact Support.</p>");
// padding needed for IE to render responses
await context.Response.WriteAsync(new string(' ', 512));
});
});
}
app.Run(context =>
{
throw new InvalidOperationException("Intentionally Breaking the Web");
});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Startup.cs
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Additional Configuration
app.UseStatusCodePages(subApp =>
{
subApp.Run(async context =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<h2>NOT FOUND!</h2>");
await context.Response.WriteAsync("<p>It's not you, it's me...</p>");
// padding needed for IE to render responses
await context.Response.WriteAsync(new string(' ', 512));
});
});
app.Run(context =>
{
context.Response.StatusCode = 404;
return Task.FromResult(0);
});
//Additional Configuration
}
Views/Home/Index.cshtml
@model middleware.Models.Home.IndexModel
<html>
<head>
</head>
<body>
<h1>Person!</h1>
<p>Name: @Model.Name </p>
<p>Age: @Model.Age</p>
</body>
</html>
appsettings.json
{
"EnvironmentDisplay": true
}
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.AspNetCore.Http;
using middleware.Models.Extensions;
namespace middleware
{
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
Configuration = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json")
.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton(typeof(IConfigurationRoot), Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler(subApp =>
{
subApp.Run(async context =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<h2>All the bad things happened!</h2>");
await context.Response.WriteAsync("<p>Application Error in Production. Contact Support</p>");
// padding needed for IE to render responses
await context.Response.WriteAsync(new string(' ', 512));
});
});
}
app.UseStatusCodePages(subApp =>
{
subApp.Run(async context =>
{
context.Response.ContentType = "text/html";
await context.Response.WriteAsync("<h2>NOT FOUND!</h2>");
await context.Response.WriteAsync("<p>It's not you, it's me...</p>");
await context.Response.WriteAsync(new string(' ', 512));
});
});
app.UseEnvironmentDisplay();
//app.Run(context =>
//{
// context.Response.StatusCode = 404;
// return Task.FromResult(0);
//});
//app.Run(context =>
//{
// throw new InvalidOperationException("Intentionally Breaking the Web");
//});
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
Of note, make sure the
app.Run()
calls are commented out.
In the constructor, theConfiguration
property is registered with dependency injection as follows:
services.AddSingleton(typeof(IConfigurationRoot), Configuration);
This ensures that the configuration can be passed into the constructor of the custom middleware class
-
In the
Models
folder, add the following: -
Extensions
MiddlewareExtensions.cs
-
Middleware
EnvironmentDisplay.cs
MiddlwareExtensions.cs
using Microsoft.AspNetCore.Builder;
using middleware.Models.Middleware;
namespace middleware.Models.Extensions
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseEnvironmentDisplay(this IApplicationBuilder builder)
{
return builder.UseMiddleware<EnvironmentDisplay>();
}
}
}
This enables the use of the extension method in the
Configure()
method ofStartup.cs
EnvironmentDisplay.cs
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using System.IO;
using System.Threading.Tasks;
namespace middleware.Models.Middleware
{
public class EnvironmentDisplay
{
private readonly IConfigurationRoot _Config;
private readonly IHostingEnvironment _Env;
private readonly RequestDelegate _Next;
public EnvironmentDisplay(RequestDelegate next, IHostingEnvironment env, IConfigurationRoot config)
{
_Next = next;
_Env = env;
_Config = config;
}
public string EnvironmentName
{
get
{
return _Env.EnvironmentName;
}
}
public bool IsEnabled
{
get
{
return _Config.GetValue("EnvironmentDisplay", false);
}
}
public async Task Invoke(HttpContext context)
{
if (!IsEnabled)
{
await _Next(context);
}
else
{
// Add the HTML for the glyph to the response
var newHeadContent = AddHead();
var newBodyContent = AddBody();
var existingBody = context.Response.Body;
using (var newBody = new MemoryStream())
{
context.Response.Body = newBody;
await _Next(context);
context.Response.Body = existingBody;
if (!context.Response.ContentType.StartsWith("text/html"))
{
await context.Response.WriteAsync(new StreamReader(newBody).ReadToEnd());
return;
}
newBody.Seek(0, SeekOrigin.Begin);
var newContent = new StreamReader(newBody).ReadToEnd();
newContent = newContent.Replace("</head>", newHeadContent + "</head>");
newContent = newContent.Replace("</body>", newBodyContent + "</body>");
await context.Response.WriteAsync(newContent);
}
}
}
private string AddHead()
{
return @"<style>
.AspNetEnv { text-indent: 25%; position: absolute; top: 0px; right: 0px; z-index: 2000; width: 200px; height: 65px; border: 1px solid red;}
.AspNetEnv_Development { background-color: green; }
.AspNetEnv_Staging { background-color: yellow; }
.AspNetEnv_Production { background-color: red; }
.AspNetEnv p { font-size: 20px; font-weight: bold; }
</style>";
}
private string AddBody()
{
return $"<div id=\"AspNetEnvIndicator\" class=\"AspNetEnv AspNetEnv_{_Env.EnvironmentName}\" title=\"{_Env.EnvironmentName}\"><p>{_Env.EnvironmentName}</p></div>";
}
}
}
All middleware requires to be used is the following method signature:
public async Task Invoke(HttpContext context)
A RequestDelegate
is a delegate function that allows the following middleware in the pipeline to be executed
The middleware Invoke
works as follows:
-
If the configuration contains the
EnvironmentDisplay
setting, and is set to true, execute, otherwise execute the following middleware in the pipeline via theRequestDelegate
. -
Pre-populate the HTML for the head and body that this middleware appends to the incoming document.
-
Capture the current body of the request in a variable.
-
Open a memeory stream, and assign the value to the response body. This enables the results of subsequent middleware changes to be captured after the delegate function is called.
-
Execute the delegate function and send the request further down the middleware pipeline.
-
After the request returns, check to make sure that it is an HTML document. If not, return the response untouched. Otherwise, continue processing the middleware logic.
-
Reset the stream position back to the beginning, then read the contents into a new variable.
-
Append the middleware head and body content into the final response body.
-
Return the response.
Execute
Change the ASPNETCORE_ENVIRONMENT
setting to Production
in launchSettings.json
:
{
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Production"
}
}
}
}
Re-execute:
Add the following directory structure:
- Services
IRequestId.cs
- Models
RequestIdMiddleware.cs
Reset Startup.cs
to the default layout:
Startup.cs
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Configuration;
namespace middleware
{
public class Startup
{
public IConfigurationRoot Configuration { get; set; }
public Startup(IHostingEnvironment env)
{
Configuration = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json")
.Build();
}
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
- Transient - Each time the service is requested, a unique instance is provided.
- Scoped - The same instance of the service is provided each time it is requested within the scope of a single HTTP request. Each piece of middleware that uses the service will receive the same instance on a single pass through the middleware stack.
- Singleton - The same instance of the service is used each time.
IRequestId.cs
using System.Threading;
namespace middleware.Services
{
public interface IRequestId
{
string Id { get; }
}
// SCOPED to the Current Request
public class RequestId : IRequestId
{
private readonly string requestId;
public RequestId(IRequestIdFactory requestIdFactory)
{
requestId = requestIdFactory.MakeRequestId();
}
public string Id => requestId;
}
public interface IRequestIdFactory
{
string MakeRequestId();
}
// SINGLETON
public class RequestIdFactory : IRequestIdFactory
{
private int requestId;
public string MakeRequestId()
{
// ++ not thread safe. Use this:
return Interlocked.Increment(ref requestId).ToString();
}
}
}
RequestIdMiddleware.cs
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using middleware.Services;
using System.Threading.Tasks;
namespace middleware.Models.Middleware
{
public class RequestIdMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<RequestIdMiddleware> _logger;
public RequestIdMiddleware(RequestDelegate next, ILogger<RequestIdMiddleware> logger)
{
_next = next;
_logger = logger;
}
public Task Invoke(HttpContext context, IRequestId requestId)
{
_logger.LogInformation($"Request {requestId.Id} executing");
return _next(context);
}
}
}
MiddlewareExtensions.cs
using Microsoft.AspNetCore.Builder;
using middleware.Models.Middleware;
namespace middleware.Models.Extensions
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseRequestIdMiddleware(this IApplicationBuilder builder)
{
return builder.UseMiddleware<RequestIdMiddleware>();
}
}
}
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddSingleton<IRequestIdFactory, RequestIdFactory>();
services.AddScoped<IRequestId, RequestId>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// Additional Configuration
app.UseRequestIdMiddleware();
// Additional Configuration
}
Views/Home/Index.cshtml
@model middleware.Models.Home.IndexModel
@inject middleware.Services.IRequestId req
<html>
<head>
</head>
<body>
<h1>Person!</h1>
<p>Name: @Model.Name </p>
<p>Age: @Model.Age</p>
<p>RequestId: @req.Id</p>
</body>
</html>
For a purely controller-driven implementation of MVC / Web API, rather than specifying
services.AddMvc()
inStartup.cs
, you can specifyservices.AddMvcCore()
and attach any additionally required functionality to this extension, for exampleservices.AddMvcCore().AddJsonFormatters()
. This would enable you to take advantage of the .NET platform on the back end, but purely use a client-side framework, such as AngularJS, for the front-end.
Add the following infrastructure:
- Models
- Products
Product.cs
- Products
- Controllers
ProductsController.cs
Product.cs
namespace middleware.Models.Products
{
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
}
}
ProductsController.cs
using Microsoft.AspNetCore.Mvc;
namespace middleware.Controllers
{
[Route("/api/[controller]")]
public class ProductsController
{
[HttpGet]
public string Get() => "Hello World";
}
}
Execute
ProductsController.cs
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using middleware.Models.Products;
// For more information on enabling MVC for empty projects, visit http://go.microsoft.com/fwlink/?LinkID=397860
namespace middleware.Controllers
{
[Route("/api/[controller]")]
[Produces("application/json")] // Force only JSON to be used
public class ProductsController : ControllerBase
{
private static List<Product> _products = new List<Product>
{
new Product { Id = 1, Name = "Computer" },
new Product { Id = 2, Name = "Radio" },
new Product { Id = 3, Name = "Apple" },
new Product { Id = 4, Name = "Pants" },
new Product { Id = 5, Name = "Tacos" }
};
[HttpGet]
public IEnumerable<Product> Get() => _products;
[HttpGet("{id}")]
public Product Get(int id)
{
var product = _products.SingleOrDefault(x => x.Id == id);
return product;
}
}
}
Execute
ProductsController.cs
[HttpGet("{id}")]
public IActionResult Get(int id)
{
var product = _products.SingleOrDefault(x => x.Id == id);
if (product == null)
{
return NotFound();
}
return Ok(product);
}
Product.cs
using System.ComponentModel.DataAnnotations;
namespace middleware.Models.Products
{
public class Product
{
public int Id { get; set; }
[Required]
public string Name { get; set; }
}
}
ProductsController.cs
[HttpPost]
public IActionResult Post([FromBody]Product product)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
_products.Add(product);
return CreatedAtAction(nameof(Get), new { id = product.Id }, product);
}
Execute
Remove the Produces
attribute from the ProductsController
:
ProductsController.cs
[Route("/api/[controller]")]
// Removed Produces Attribute
public class ProductsController : ControllerBase
// Rest of the controller logic
Add the Microsoft.AspNetCore.Mvc.Formatters.Xml
package to project.json
:
project.json
{
"dependencies": {
"Microsoft.AspNetCore.Mvc.Formatters.Xml": "1.1.0"
}
}
Update the services.AddMvc()
call in Startup.cs
to add the XML formatter:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddXmlDataContractSerializerFormatters();
}
Debug and launch Postman. Change the header to Accept - application/xml
on the Headers
tab:
This section is mainly concerned with how to scaffold an Angular2 app in ASP.NET Core, and thus little will be covered in the way of Angular2 itself. An in-depth look at Angular2 will be made in an upcoming document.
Install Node.js (if it's not already)
Install Yeoman and the ASP.NET Core SPA Generator:
npm install -g yo generator-aspnetcore-spa
Install Webpack:
npm install -g webpack
Create the Project Directory:
{directory}>mkdir {app-dir}
{directory}>cd {app-dir}
Run the ASP.NET Core SPA generator in Yeoman:
{app-dir}>yo aspnetcore-spa
Select Angular and hit Enter
Select project.json
and hit Enter
Type n for no unit tests, then hit Enter
Optionally provide a name for the project (if different from the directory created above), then hit Enter
When the installation has completed, set the ASPNETCORE_ENVIRONMENT
variable:
{app-dir}>set ASPNETCORE_ENVIRONMENT=Development
Open the project in Visual Studio Code:
{app-dir}>code .
Run the application using dotnet watch
so that the application will rebuild any time changes are made as it's being run:
{app-dir}>dotnet watch run
Basic Angular2 Structure Overview
The Angular2 application exists in the ClientApp/app
directory of the project, and the pieces of the application are built in the components
directory.
app.module.ts
is where dependent modules and components are declared, and the app is configured:
app.module.ts
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { UniversalModule } from 'angular2-universal';
import { AppComponent } from './components/app/app.component'
import { NavMenuComponent } from './components/navmenu/navmenu.component';
import { HomeComponent } from './components/home/home.component';
import { FetchDataComponent } from './components/fetchdata/fetchdata.component';
import { CounterComponent } from './components/counter/counter.component';
@NgModule({
bootstrap: [ AppComponent ],
declarations: [
AppComponent,
NavMenuComponent,
CounterComponent,
FetchDataComponent,
HomeComponent
],
imports: [
UniversalModule, // Must be first import. This automatically imports BrowserModule, HttpModule, and JsonpModule too.
RouterModule.forRoot([
{ path: '', redirectTo: 'home', pathMatch: 'full' },
{ path: 'home', component: HomeComponent },
{ path: 'counter', component: CounterComponent },
{ path: 'fetch-data', component: FetchDataComponent },
{ path: '**', redirectTo: 'home' }
])
]
})
export class AppModule {
}
The app
folder in the components
directory defines application wide logic, markup, and stylings.
app.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
}
app.component.css
@media (max-width: 767px) {
/* On small screens, the nav menu spans the full width of the screen. Leave a space for it. */
.body-content {
padding-top: 50px;
}
}
app.component.html
<div class='container-fluid'>
<div class='row'>
<div class='col-sm-3'>
<nav-menu></nav-menu>
</div>
<div class='col-sm-9 body-content'>
<router-outlet></router-outlet>
</div>
</div>
</div>
The app.component.html
markup is essentially the master template for the application. Navigation is managed by the nav-menu
component, and the router-outlet
component is used to render components based on the routing encountered by Angular2. If you look at the imports
section of the app.module.ts - @NgModule
definintion, you can see how routes are defined and determine the component that will be rendered in the router-outlet
depending on the route that is encountered.
The remainder of this section will look at the nav-menu
and fetch-data
components.
navmenu.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'nav-menu',
templateUrl: './navmenu.component.html',
styleUrls: ['./navmenu.component.css']
})
export class NavMenuComponent {
}
navmenu.component.css
li .glyphicon {
margin-right: 10px;
}
/* Highlighting rules for nav menu items */
li.link-active a,
li.link-active a:hover,
li.link-active a:focus {
background-color: #4189C7;
color: white;
}
/* Keep the nav menu independent of scrolling and on top of other items */
.main-nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1;
}
@media (min-width: 768px) {
/* On small screens, convert the nav menu to a vertical sidebar */
.main-nav {
height: 100%;
width: calc(25% - 20px);
}
.navbar {
border-radius: 0px;
border-width: 0px;
height: 100%;
}
.navbar-header {
float: none;
}
.navbar-collapse {
border-top: 1px solid #444;
padding: 0px;
}
.navbar ul {
float: none;
}
.navbar li {
float: none;
font-size: 15px;
margin: 6px;
}
.navbar li a {
padding: 10px 16px;
border-radius: 4px;
}
.navbar a {
/* If a menu item's text is too long, truncate it */
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
navmenu.component.html
<div class='main-nav'>
<div class='navbar navbar-inverse'>
<div class='navbar-header'>
<button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'>
<span class='sr-only'>Toggle navigation</span>
<span class='icon-bar'></span>
<span class='icon-bar'></span>
<span class='icon-bar'></span>
</button>
<a class='navbar-brand' [routerLink]="['/home']">AngularSpa</a>
</div>
<div class='clearfix'></div>
<div class='navbar-collapse collapse'>
<ul class='nav navbar-nav'>
<li [routerLinkActive]="['link-active']">
<a [routerLink]="['/home']">
<span class='glyphicon glyphicon-home'></span> Home
</a>
</li>
<li [routerLinkActive]="['link-active']">
<a [routerLink]="['/counter']">
<span class='glyphicon glyphicon-education'></span> Counter
</a>
</li>
<li [routerLinkActive]="['link-active']">
<a [routerLink]="['/fetch-data']">
<span class='glyphicon glyphicon-th-list'></span> Fetch data
</a>
</li>
</ul>
</div>
</div>
</div>
The anchor
tags use the [routerLink]
directive to link routing to the angular routing module. When clicked, the routing module will determine the appropriate component to load into the router-outlet
component region. The routes are defined in the app's module
definition.
fetchdata.component.ts
import { Component } from '@angular/core';
import { Http } from '@angular/http';
@Component({
selector: 'fetchdata',
templateUrl: './fetchdata.component.html'
})
export class FetchDataComponent {
public forecasts: WeatherForecast[];
constructor(http: Http) {
http.get('/api/SampleData/WeatherForecasts').subscribe(result => {
this.forecasts = result.json() as WeatherForecast[];
});
}
}
interface WeatherForecast {
dateFormatted: string;
temperatureC: number;
temperatureF: number;
summary: string;
}
fetchdata.compnent.html
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from the server.</p>
<p *ngIf="!forecasts"><em>Loading...</em></p>
<table class='table' *ngIf="forecasts">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let forecast of forecasts">
<td>{{ forecast.dateFormatted }}</td>
<td>{{ forecast.temperatureC }}</td>
<td>{{ forecast.temperatureF }}</td>
<td>{{ forecast.summary }}</td>
</tr>
</tbody>
</table>
The fetch-data
component defines a WeatherForecast
interface and specifies a variable forecasts
which is an array of WeatherForecasts
. This module also makes use of Angular2's Http
module, injects the module into the component constructor, and makes a call to an ASP.NET Core API route to populate the forecasts
variable with data using the http.get()
method.
In the markup for the component, while the forecasts
array is empty, the component simply displays Loading...
. After the forecasts
array has been populated, a table iterates through all of the available forecasts and displays their data.
For completion, here is the SampleDataController
:
SampleDataController.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
namespace AngularSpa.Controllers
{
[Route("api/[controller]")]
public class SampleDataController : Controller
{
private static string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
[HttpGet("[action]")]
public IEnumerable<WeatherForecast> WeatherForecasts()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
DateFormatted = DateTime.Now.AddDays(index).ToString("d"),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
});
}
public class WeatherForecast
{
public string DateFormatted { get; set; }
public int TemperatureC { get; set; }
public string Summary { get; set; }
public int TemperatureF
{
get
{
return 32 + (int)(TemperatureC / 0.5556);
}
}
}
}
}
Click to see application running
Entity Framework is an Object Relational Mapper (ORM) for .NET Framework. It bridges the gap between C# and SQL.
In Visual Studio, create a new ASP.NET Core project
Web App template with no Authentication
View > SQL Server Object Explorer
Expand SQL Server > (localdb)\mssqllocaldb
, right-click Databases
and click Add New Database
. Set the name as Products
then click OK.
Expand the Products
database, right-click Tables
, and click Add New Table
In the T-SQL
pane at the bottom of the new table window, change [dbo].[Table]
to [dbo].[Category]
Also, configure the primary key (auto increment by one starting at one) on line 2: [Id] INT IDENTITY(1,1) NOT NULL
Then, configure the table as follows:
With everything set, click Update
. In the window that pops up, click Update Database
.
Right-click Tables
, and click Add New Table
In the T-SQL
pane at the bottom of the new table window, change [dbo].[Table]
to [dbo].[Product]
Also, configure the primary key (auto increment by one starting at one) on line 2: [Id] INT IDENTITY(1,1) NOT NULL
Then, configure the table as follows:
Right-click Foreign Keys
in the pane on the right, click Add New Foreign Key
, and name it FK_Product_ToCategory
. In the T-SQL pane, set the foreign key constraint as follows:
CONSTRAINT [FK_Product_ToCategory] FOREIGN KEY ([CategoryId]) REFERENCES [Category]([Id])
With all of the configuration set, click Update
. In the window that pops up, click Update Database
.
Right-click the Category
table, and click View Data
. Add the data as shown:
Right-click the Product
table, and click View Data
. Add the data as shown:
Add Entity Framework
dependencies in project.json
project.json
{
"dependencies": {
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.Design": "1.0.0-preview2-final",
"Microsoft.EntityFrameworkCore.SqlServer.Design": "1.0.0",
},
"tools": {
"Microsoft.EntityFrameworkCore.Tools": "1.0.0-preview2-final"
}
}
Execute the following command in Package Manager Console
Scaffold-DbContext "Server=(localdb)\mssqllocaldb;Database=Products;Trusted_Connection=True" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
Add the ConnectionStrings
configuration to appsettings.json
appsettings.json
{
"ConnectionStrings": {
"Products": "Server=(localdb)\\mssqllocaldb;Database=Products;Trusted_Connection=True;"
}
}
In Models/ProductsContext.cs
, delete the OnConfiguring
override, and add a constructor that injects DbContextOptions<ProductsContext>
ProductsContext.cs
public ProductsContext(DbContextOptions<ProductsContext> options) : base(options) { }
Configure the Entity Framework service in Startup.cs
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// Additional Configuration
var cs = Configuration.GetConnectionString("Products");
services.AddDbContext<Products>(options =>
options.UseSqlServer(cs)
);
}
Update _Layout.cshtml
navbar with the Products
link
_Layout.cshtml
<!-- Preceding HTML -->
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">EFCore</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-area="" asp-controller="Products" asp-action="Index">Products</a></li>
</ul>
<form id="searchForm" class="navbar-form navbar-left" role="search">
<div class="form-group">
<input id="searchTerm" class="form-control" type="text" placeholder="Search" />
</div>
<button class="btn btn-default" type="submit">Go</button>
</form>
</div>
</div>
</div>
<!-- Following HTML -->
<!-- Just above @RenderSection() -->
<script>
$(document).ready(function () {
$('#searchForm').submit(function (event) {
var url = '@Url.Action("Search", "Products")?term=' + $('#searchTerm').val();
window.location.href = url;
return false;
});
})
</script>
<!-- Remaining HTML -->
Add a ProductsController.cs
class to the Controllers
folder
ProductsController.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using EFCore.Models;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace EFCore.Controllers
{
public class ProductsController : Controller
{
ProductsContext _context;
public ProductsController(ProductsContext context)
{
_context = context;
}
public async Task<IActionResult> Index()
{
var products = await _context.Product.OrderBy(x => x.Name).ToListAsync();
return View(products);
}
public async Task<IActionResult> Details(int id)
{
var prod = await _context.Product.FirstOrDefaultAsync(x => x.Id == id);
return View(prod);
}
public async Task<IActionResult> Search(string term)
{
// Example of executing FromSql if table valued function is available
// Because using localdb, cannot use CONTAINS in where clause because a full text index cannot be created
// var products = await _context.Product.FromSql("SELECT * FROM dbo.SearchProducts({0})", term).OrderBy(x => x.Name).ToListAsync();
// Use Linq instead
var products = await _context.Product
.Where(x => x.Name.ToLower().Contains(term.ToLower()) || x.Description.ToLower().Contains(term.ToLower()))
.OrderBy(x => x.Name)
.ToListAsync();
return View(products);
}
public Task<IActionResult> Create()
{
return Task.Run(() =>
{
ViewBag.CategoryId = new SelectList(_context.Category, "Id", "Name");
return (IActionResult)View();
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Product product)
{
if (ModelState.IsValid)
{
_context.Product.Add(product);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(product);
}
public async Task<IActionResult> Edit(int id)
{
var product = await _context.Product.FirstOrDefaultAsync(x => x.Id == id);
ViewBag.CategoryId = new SelectList(_context.Category, "Id", "Name");
return View(product);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(Product product)
{
if (ModelState.IsValid)
{
_context.Update(product);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(product);
}
[ActionName("Delete")]
public async Task<IActionResult> Delete(int id)
{
var product = await _context.Product.FirstOrDefaultAsync(x => x.Id == id);
return View(product);
}
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var product = await _context.Product.FirstOrDefaultAsync(x => x.Id == id);
_context.Product.Remove(product);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
}
}
Create a Views\Products
folder, then add views for each of the GET
action methods in the ProductsController
Index.cshtml
@model IEnumerable<EFCore.Models.Product>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<tbody>
@foreach (var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Details.cshtml
@model EFCore.Models.Product
@{
ViewData["Title"] = "Details";
}
<h2>Details</h2>
<h4>Product</h4>
<hr />
<dl class="dl-horizontal">
<dt>@Html.LabelFor(modelItem => @Model.Description)</dt>
<dd>@Html.DisplayFor(modelItem => @Model.Description)</dd>
<dt>@Html.LabelFor(modelItem => @Model.Name)</dt>
<dd>@Html.DisplayFor(modelItem => @Model.Name)</dd>
<dt>@Html.LabelFor(modelItem => @Model.Price)</dt>
<dd>@Html.DisplayFor(modelItem => @Model.Price)</dd>
</dl>
<div>
<a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> |
<a asp-action="Index">Back to List</a>
</div>
Create.cshtml
@model EFCore.Models.Product
@{
ViewBag.Title = "Create";
}
<h2>Create</h2>
<form asp-action="Create" method="post">
<div class="form-horizontal">
<h4>Product</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Category" class="control-label col-md-2"></label>
<div class="col-md-10">
<select asp-for="CategoryId" asp-items="@ViewBag.CategoryId" class="form-control"></select>
<span asp-validation-for="CategoryId" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label col-md-2"></label>
<div class="col-md-10">
<textarea asp-for="Description" class="form-control"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Price" type="number" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
Search.cshtml
@model IEnumerable<EFCore.Models.Product>
@{
var query = Context.Request.Query["term"];
}
<h2>Search</h2>
<h4>Searching for: @query</h4>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<tbody>
@foreach(var item in Model)
{
<tr>
<td>
@Html.DisplayFor(modelItem => item.Name)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
<a asp-action="Details" asp-route-id="@item.Id">Details</a> |
<a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
</td>
</tr>
}
</tbody>
</table>
Edit.cshtml
@model EFCore.Models.Product
@{
ViewBag.Title = "Edit";
}
<h2>Edit</h2>
<form asp-action="Edit" method="post">
<div class="form-horizontal">
<h4>Product</h4>
<hr />
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="Name" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Category" class="control-label col-md-2"></label>
<div class="col-md-10">
<select asp-for="CategoryId" asp-items="@ViewBag.CategoryId" class="form-control"></select>
<span asp-validation-for="CategoryId" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label col-md-2"></label>
<div class="col-md-10">
<textarea asp-for="Description" class="form-control"></textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="Price" class="control-label col-md-2"></label>
<div class="col-md-10">
<input asp-for="Price" type="number" class="form-control" />
<span asp-validation-for="Price" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Update" class="btn btn-default" />
</div>
</div>
</div>
</form>
<div>
<a asp-action="Index">Back to List</a>
</div>
Delete.cshtml
@model EFCore.Models.Product
@{
ViewBag.Title = "Delete";
}
<h2>Delete</h2>
<h4>Are you sure you want to delete @Model.Name?</h4>
<form asp-action="Delete">
<div class="btn-toolbar">
<div class="btn-group">
<input type="submit" value="Delete" class="btn btn-danger" />
</div>
<div class="btn-group">
<a asp-action="Index" class="btn btn-default">Back to List</a>
</div>
</div>
</form>
Run the application and check out each of the different action methods
Create a new ASP.NET Core Project
Select Web App template, No Authentication and Host in Cloud
You will need to have an azure account associated with the Microsoft account used to authenticate to Visual Studio. By joining Visual Studio Dev Essentials, you can get a free $25 dollar credit per month for a year. You can also setup Azure to cap spending at the amount provided by this service.
Before you can configure the app service, you'll need to create a resource group on you azure profile at the Azure Portal
On the Azure dashboard, click Resource Groups from the services list on the left side
Click Add on the Resource Groups management page
Give the resource group a name, select the associated azure subscription, select the region to host the service from, then click Create
In Visual Studio, from the Create App Service window, click New next to App Service Plan. Provide a name for the service plan, select the region where the service plan will be hosted, and the size of the app service plan.
Back in the Create App Service window, provide a name for the web app, select your azure subscription, select the resource group created above, and select the app service plan created above. Click Create to scaffold the web app and provision resources in Azure for the web app to be published.
Modify Views\Home\Index.cshtml
Index.cshtml
@{
ViewData["Title"] = "Home Page";
}
<h2>Virtual Academy Demo</h2>
Either right-click the VirtualAcademy web app project, and click Publish, or from the top menu, click Build then click Publish VirtualAcademy
The initial setup that was done by selecting Host in Cloud when scaffolding the project created a publish profile for the web app. Click Next
The Settings page allows you to set the configuration of the deployment, the target framework version, and specify additional File Publish Options and Database Connection Strings. Click Next
In the Preview window, you can click Start Preview to see the files affected by the publish and the action that will be taken as a result of the publish. Click Publish
Once the web app has been published, the web app will be opened from the location which it was published
In order to complete this section, a GitHub account is needed and Git needs to be installed on the local machine. See Version Control in the Visual Studio Code docs for details on working with Git in Code.
Configure an ASP.NET Core SPA
{dir}>mkdir {app-dir}
{dir}>cd {app-dir}
{app-dir}>yo aspnetcore-spa
Select Angular > project.json > n (no tests)
Open in VS Code with {app-dir}>code .
When the prompt to Add required assets to build and debug
shows up, click Yes
Click the Git icon in the side bar, then click Initialize Repository
Click the check icon, or press Ctrl + Enter, to Commit All
Login to GitHub and create a repo
Copy the URL that is generated after the repo is created
In the integrated terminal in VS Code (open by pressing Ctrl + ` <- backtick, not apostrophe) and enter the following
git remote add origin <url>
git push -u origin master
From the Git page on the side bar (where the repo was initialized and the assets were committed), click the ... dropdown, then click Sync
Refresh the repo page on GitHub, and see all of the assets loaded
On the Azure Portal, click New Item
Click Web + Mobile and select Web App
Configure the web app by providing a name, setting the associated azure subscription, create a new resource group name, and configure the app service plan. When configuring the app service plan, provide a name, region to host the service, and pricing tier (I'm using the Free pricing tier). Click Create when finished.
Open the web app dashboard, click Deployment Options, set the source as GitHub, then click OK
Under Authorization, click Authorize and to allow Azure permission to access your GitHub account. Once authorized, click OK at the bottom of the Authorization window.
At Choose project, select your GitHub repo and leave the branch as master, then click OK at the bottom of the Deployment source window to begin web app deployment
From the web app dashboard, click Deployment options again, and you'll see the app being built. Once the build is successful, you will be able to navigate to the live version of the web app with corresponding URL.