Skip to content

Instantly share code, notes, and snippets.

@JaimeStill
Last active July 9, 2017 13:23
Show Gist options
  • Save JaimeStill/d30a02e6bab40226668285a3a898f481 to your computer and use it in GitHub Desktop.
Save JaimeStill/d30a02e6bab40226668285a3a898f481 to your computer and use it in GitHub Desktop.
MVA Intermediate ASP.NET Core 1.0

This series continues from the app that is built in the Introduction to ASP.NET Core course, starting at the Creating a Form section.

Contents

Introduction to Tag Helpers
Authentication
Custom Middleware
Dependency Injection
APIs and MVC Core
SPAs and Angular2
Entity Framework Core
Publishing and Deployment

Links

Tools

Postman Chrome Extension

References

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

Back to Top

<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:

Non-HTML Elements

  • Cache
  • Environment
  • ValidationMessage
  • ValidationSummary

HTML Elements

  • Anchor
  • Form
  • Input
  • Label
  • Link
  • Option
  • Script
  • Select
  • TextArea

Tag helpers don't have to necessarily augment an existing HTML tag.

Using Tag Helpers

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.

Custom Tag Helper

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 the mvc-form name rather than the mvc_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.

Back to Top

Setup

  • Create new ASP.NET Core Web App Project

authentication-project

  • Select Web Application template, then click Change Authentication

authentication-template

  • Select Individual Authentication from the authentication options, then create the project

authentication-mechanism

Authentication-Related Template Files

  • Controllers
    • AccountController.cs
    • ManageController.cs
  • Data
    • Migrations
      • xxx_CreateIdentitySchema.cs
      • ApplicationDbContextModelSnapshot.cs
    • ApplicationDbContext.cs
  • 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
  • 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

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
}

Configure SSL Development

  • Right-click the project, and click Properties, then click the Debug tag. From here, check the Enable SSL option at the bottom.

authentication-ssl

  • In launchSettings.json, configure the IIS Express profile's launchUrl attribute to make use of the URL pointing to the sslPort value specified in iisSettings:
{
  "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

authentication-trust-ssl

  • Install the self-signed SSL Certificate

authentication-install-cert

Execute

authentication-execute-with-ssl

Local User Authentication

  • Execute the application, and click the Register link at the right side of the navbar at the top

authentication-register

  • 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.

authentication-database-error

  • Click the Apply Migrations button to execute the migrations

authentication-apply-migrations

  • Once the migration has completed, refresh the page and notice that a user account has been authenticated

authentication-migrations-refresh

  • 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.

authentication-services

authentication-facebook-service

  • Fill in the details of the Create a New App ID form, then click Create App ID

authentication-create-new-app

  • The App ID is generated, and authentication now needs to be configured. In the Product Setup region, click Get Started under Facebook Login

authentication-facebook-app

authentication-oauth-redirect

  • 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.

authentication-fbapp-dashboard

  • 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}

authentication-store-secrets

  • In project.json, register Microsoft.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
    authentication-facebook-service

  • Click Continue as {Your Name Here}

authentication-facebook-verification

  • Enter an email to associate the account with your account on the web app, then click Register

authentication-associate-facebook

authentication-facebook

Back to Top

Starting Point Setup

  • New ASP.NET Core Web App Project

middleware-new-project

  • Empty Web Template

middeware-empty-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
  • Views

    • Home
      • Index.cshtml

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>

Inline Middleware - HTTP Response

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?}");
    });
}

Inline Middleware - HTTP Status Codes

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
}

Execute
middleware-404

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, the Configuration 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

Building the Middleware Components

  • 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 of Startup.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:

  1. If the configuration contains the EnvironmentDisplay setting, and is set to true, execute, otherwise execute the following middleware in the pipeline via the RequestDelegate.

  2. Pre-populate the HTML for the head and body that this middleware appends to the incoming document.

  3. Capture the current body of the request in a variable.

  4. 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.

  5. Execute the delegate function and send the request further down the middleware pipeline.

  6. 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.

  7. Reset the stream position back to the beginning, then read the contents into a new variable.

  8. Append the middleware head and body content into the final response body.

  9. Return the response.

Execute

middleware-development

Change the ASPNETCORE_ENVIRONMENT setting to Production in launchSettings.json:

{
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Production"
      }
    }
  }
}

Re-execute:

middleware-production

Back to Top

Dependency Injection Details

Setup

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?}");
            });
        }
    }
}

Injecting Middleware Services

Dependency Injection Service Lifetimes

  • 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.

Service Interfaces and Classes

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();
        }
    }
}

Middleware Definition

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>();
        }
    }
}

Implementation

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
}

Injection in Views

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>

Execute with Kestrel

di-console

di-page

Back to Top

For a purely controller-driven implementation of MVC / Web API, rather than specifying services.AddMvc() in Startup.cs, you can specify services.AddMvcCore() and attach any additionally required functionality to this extension, for example services.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.

Setup

Add the following infrastructure:

  • Models
    • Products
      • Product.cs
  • Controllers
    • ProductsController.cs

Product.cs

namespace middleware.Models.Products
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
}

Simple Controller-less Controller

ProductsController.cs

using Microsoft.AspNetCore.Mvc;

namespace middleware.Controllers
{
    [Route("/api/[controller]")]
    public class ProductsController
    {
        [HttpGet]
        public string Get() => "Hello World";
    }
}

Execute

api-controllerless

Basic Web API Controller

Postman Chrome Extension

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

api-get-products

api-get-product

Provided RESTful GET Responses

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);
}

Validating and Adding Objects With POST

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

api-post-product

api-post-get

Posting a Bad Request

api-post-bad-request

Add XML Formatter

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:

api-set-xml

Execute
api-get-xml

Back to Top

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.

Setup

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

ng-generator

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

ng-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.

Module Definition

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 {
}

App Component

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.

Nav Menu Component

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.

Fetch Data Component

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);
                }
            }
        }
    }
}
Angular App Running

Click to see application running
angular2-spa

Back to Top

Entity Framework is an Object Relational Mapper (ORM) for .NET Framework. It bridges the gap between C# and SQL.

Project / Database Setup

In Visual Studio, create a new ASP.NET Core project

ef-new-project

Web App template with no Authentication

ef-new-template

View > SQL Server Object Explorer

ef-sql-explorer

Expand SQL Server > (localdb)\mssqllocaldb, right-click Databases and click Add New Database. Set the name as Products then click OK.

ef-create-database

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:

ef-category

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:

ef-product

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:

ef-category-data

Right-click the Product table, and click View Data. Add the data as shown:

ef-product-data

Import Existing Database

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

Execute
ef-index

Back to Top

Deploy to Azure from Visual Studio

Create a new ASP.NET Core Project

pub-new-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.

pub-new-template

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

pub-azure-dashboard

Click Add on the Resource Groups management page

pub-resource-groups

Give the resource group a name, select the associated azure subscription, select the region to host the service from, then click Create

pub-add-resource-group

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.

pub-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.

pub-create-app-service

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

pub-publish-connection

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

pub-publish-settings

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

pub-publish-preview

Once the web app has been published, the web app will be opened from the location which it was published

Click to See Live Web App
pub-deployed

Integrating GitHub with Azure

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

git-add-assets

Click the Git icon in the side bar, then click Initialize Repository

git-initialize

Click the check icon, or press Ctrl + Enter, to Commit All

git-commit

Login to GitHub and create a repo

git-create-repo

Copy the URL that is generated after the repo is created

git-repo-url

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

git-sync

Refresh the repo page on GitHub, and see all of the assets loaded

git-repo

On the Azure Portal, click New Item

azure-new-item

Click Web + Mobile and select Web App

azure-new-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.

azure-web-app-config

Open the web app dashboard, click Deployment Options, set the source as GitHub, then click OK

azure-web-app-dashboard

azure-deployment-source

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.

azure-authorization

azure-authorize

azure-authorized

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

azure-project

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.

azure-building

azure-built

azure-web-app

Back to Top

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment