Starting a new dotnet core project

From Logic Wiki
Revision as of 15:26, 2 May 2017 by AliIybar (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to: navigation, search

Introduction

In this project, I'll use Visual Studio CODE as code-editor. Each concept will be explained in individual header and will be in its own branch

Only Data Models will be in a different project

Setting Up

Code Extensions

in Code hit Shift + Control + P and type Install Extensions to install new extensions

  • ASP.NET Helper (schneiderpat)
  • Auto Import (steoates)
  • Beautify (HookyQR)
  • C# (Microsoft)
  • C# Extensions (jchannon)
  • mssql (icrosoft) (maybe)
  • REST Client (Huachao Mao)

Source Control

Bitbucket is used. After creating a new repository I went back to my local folder and in git-bash I did these below.

cd logicbase
git init
git remote add origin https://USERNAME:PASSWORD@bitbucket.org/ali_iybar/logicbase.git

once I created my folder structure I'll use the line below but it's too early for now.

git push -u origin master

Install Node.js, npm, and Yeoman

Prerequisites

Node.js and npm are required for Yeoman. Download from Node.js. The installer includes Node.js and npm. Bower is also required for installing UI components like stylesheets. To install Yeoman and Bower run the following command:

npm install -g yo bower

From a command prompt, install the ASP.NET generator:

npm install -g generator-aspnet@latest

Creating Project

in command prompt, under logicbase

yo aspnet 

and move to Web API Application with arrow keys

give a name LogicBase.API


GIT : New API Project c4c7aa2


Create models project

yo aspnet 

and move to Class Library with arrow keys

give a name LogicBase.Models


GIT : Models Project Is Added d8c88a3


MySQL

Package Reference

in LogicBase.Api project go to csproj file and add this line

<PackageReference Include="SapientGuardian.EntityFrameworkCore.MySql" Version="7.1.21" />

Go to command prompt and go to folder LogicBase.API and run

dotnet restore
dotnet build

Create a class derived from DbContext

create Data folder Create a LogicDbContext.cs file under Data folder and write them in

using Microsoft.EntityFrameworkCore;

namespace LogicBase.API.Core.Data
{
    public class LogicDbContext :DbContext
    {
         public LogicDbContext(DbContextOptions<LogicDbContext> options) 
           : base(options)
       {}
    }
}

Startup.cs

in StartUp.cs add this line first

using MySQL.Data.Entity.Extensions;

and in ConfigureServices method add these lines

            services.AddDbContext<LogicDbContext>(options =>
                 options.UseMySQL(Configuration.GetConnectionString("DefaultConnection"),
       optionsBuilder => optionsBuilder.MigrationsAssembly("LogicBase.API")));

Connection String

We need to define connection parameters in appsettings.json file

  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=EfCoreSample;userid=root;pwd=esref;port=3306;sslmode=none"
  },

We need to add the line below to LogicBase.API.csproj file to use Models in API project

  <ItemGroup>
    <ProjectReference Include="..\Logicbase.Models\Logicbase.Models.csproj" />
  </ItemGroup>  

We are nearly there. We just need a model to test our connection

In Models project I've added an error.cs model and in LogicDbContext file I added the line below in LogicDbContext class

public DbSet<Error> Errors { get; set; }

Migration

Add them to LogicBase.API.csproj file

  <ItemGroup>
    <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="1.1.1" PrivateAssets="All" />
  </ItemGroup>
  <ItemGroup>
    <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="1.0.0" />
  </ItemGroup>

in command prompt

dotnet ef migrations add FirstMigration 

and

dotnet ef database update

GIT : MySql Implementation cd5cb8f


Patterns

[Repository Pattern] and [Unit Of Work Pattern] implemented.


GIT : Repository and UnitOfWork patterns implemented e3321f3


.NET Identity

in LogicBase.API.csproj and Logicbase.Models files add these lines

<PackageReference Include="Microsoft.AspNetCore.Identity" Version="1.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="1.1.1" />

Create a LogicUser model in Logicbase.Models project

using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
namespace Logicbase.Models.DataModels.User
{
    public class LogicUser: IdentityUser
    {
        
    }
}

In LogicBase.API/Startup.cs file

using Logicbase.Models.DataModels.User;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;

 public void ConfigureServices(IServiceCollection services)
 {
   services.AddIdentity<LogicUser, IdentityRole>()
     .AddEntityFrameworkStores<LogicDbContext>()
     .AddDefaultTokenProviders();

   services.Configure<IdentityOptions>(options => {
      options.Password.RequireDigit = false;
      options.Password.RequiredLength = 6;
      options.Password.RequireLowercase = false;
      options.Password.RequireNonAlphanumeric = false;
      options.Password.RequireUppercase = false;

      options.SignIn.RequireConfirmedEmail = true;

      options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
      options.Lockout.MaxFailedAccessAttempts = 10;
 
      options.Cookies.ApplicationCookie.ExpireTimeSpan = TimeSpan.FromDays(150);
      // User settings
      options.User.RequireUniqueEmail = true;
    });

    services.AddTransient<IMessageService, MessageService>();
}

 public void Configure(......
 {
    app.UseIdentity();
 }


Create IMessageService.cs in LogicBase.API project

using System.Threading.Tasks;
namespace LogicBase.API.Core.Business.Interfaces
{
    public interface IMessageService
    {
        Task Send(string email, string subject, string message);    
    }
}

Account Registration

View Models

    public class ApplicationUser
    {
        public string UserName { get; set; }
        public string Password { get; set; }
    }
    public class ForgotPasswordViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }

        public string ResetPasswordUrl { get; set; }
    }
    public class LoginViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }

        [Required]
        [DataType(DataType.Password)]
        public string Password { get; set; }

        [Display(Name = "Remember me?")]
        public bool RememberMe { get; set; }
    }
    public class ResetPasswordViewModel
    {
        [Required]
        [EmailAddress]
        public string Email { get; set; }
        [Required]
        [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [DataType(DataType.Password)]
        public string Password { get; set; }
        [DataType(DataType.Password)]
        [Display(Name = "Confirm password")]
        [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
        public string ConfirmPassword { get; set; }
        public string Code { get; set; }
    }

Controller

using System.Collections.Generic;
using System.Threading.Tasks;
using Logicbase.Models.DataModels.User;
using LogicBase.API.Core.Business.BusinessLogic;
using LogicBase.API.Core.Business.Interfaces;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Authorization;
using Logicbase.Models.ViewModels.Account;
using System.Linq;
using Newtonsoft.Json;

namespace LogicBase.API.Controllers
{
    [Route("api/[controller]/[action]")]
    public class IdentityController : Controller
    {
        private readonly UserManager<LogicUser> _userManager;
        private readonly SignInManager<LogicUser> _signInManager;
        private readonly IMessageService _messageService;
        private readonly ILogger _logger;
        private readonly JsonSerializerSettings _serializerSettings;        
        public IdentityController(UserManager<LogicUser> userManager,
                                    ILoggerFactory loggerFactory,
                                    SignInManager<LogicUser> signInManager,
                                    IMessageService messageService)
        {
            this._userManager = userManager;
            this._signInManager = signInManager;
            this._messageService = messageService;
            _logger = loggerFactory.CreateLogger<IdentityController>();
             _serializerSettings = new JsonSerializerSettings
            {
                Formatting = Formatting.Indented
            };
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> Register(string email, string password, string repassword, string verificationUrl)
        {
            if (password != repassword)
            {
                return BadRequest(ErrorManager.GetError(101));
            }

            var newUser = new LogicUser()
            {
                UserName = email,
                Email = email
            };

            var userCreationResult = await _userManager.CreateAsync(newUser, password);
            if (!userCreationResult.Succeeded)
            {
                var moreDetails = userCreationResult.Errors.Aggregate("", (current, error) => current + (error.Description + "\n"));
                return BadRequest(ErrorManager.GetError(102, moreDetails));
            }

            var emailConfirmationToken = await _userManager.GenerateEmailConfirmationTokenAsync(newUser);
            var tokenVerificationUrl = Url.RouteUrl(verificationUrl,
                new {userId = newUser.Id, token = emailConfirmationToken}, Request.Scheme);

            await _messageService.Send(email, "Verify your email",
                $"Click <a href=\"{tokenVerificationUrl}\">here</a> to verify your email");

            return Ok();
           
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> VerifyEmail(string userId, string token)
        {
            if (userId == null || token == null)
            {
                return BadRequest(ErrorManager.GetError(100));
            }
            var user = await _userManager.FindByIdAsync(userId);
            if (user == null)
            {
                return BadRequest(ErrorManager.GetError(103));
            }
            var result = await _userManager.ConfirmEmailAsync(user, token);
            if (result.Succeeded)
            {
                return Ok();
            }
            else
            {
                return BadRequest(ErrorManager.GetError(103));
            }
                
        }

        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = await _userManager.FindByEmailAsync(model.Email);
                if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
                {
                    return BadRequest(ErrorManager.GetError(100));
                }

               var code = await _userManager.GeneratePasswordResetTokenAsync(user);
              // var callbackUrl = Url.Action(nameof(ResetPassword), "Accounts", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
                var callbackUrl = Url.RouteUrl(model.ResetPasswordUrl, new {userId = user.Id, code = code},
                    protocol: HttpContext.Request.Scheme);
                await _messageService.Send(model.Email, "Reset Password", $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");// _emailSender.SendEmailAsync(model.Email, "Reset Password", $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
                return Ok();
            }

            // If we got this far, something failed, redisplay form
            return BadRequest();
        }


        [HttpPost]
        [AllowAnonymous]
        public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest();
            }
            var user = await _userManager.FindByEmailAsync(model.Email);
            if (user == null)
            {
                // Don't reveal that the user does not exist
                return BadRequest();
            }
            var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password);
            if (result.Succeeded)
            {
                return Ok();
            }
            return BadRequest();
        }

    }
}


GIT : .NET Identity Implemented a0e2f77


Swagger Implementation

[Integrate Swagger into ASP.NET Core]


GIT : Swagger Implemented a1900d0


Token Implementation

add this line to both csproj files

<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="1.1.1" />

Add this to startup.cs

 services.AddMvc(config =>
 {
   var policy = new AuthorizationPolicyBuilder()
      .RequireAuthenticatedUser()
      .Build();
   config.Filters.Add(new AuthorizeFilter(policy));
 });    

I already added ApplicationUser model as below.

public class ApplicationUser
{
  public string UserName { get; set; }
  public string Password { get; set; }
}

Add a new model to Logicbase.Models project

using System;
using System.Collections.Generic;
using Microsoft.IdentityModel.Tokens;
using System.Threading.Tasks;

namespace Logicbase.Models.DataModels.Security
{
 public class JwtIssuerOptions
    {
        /// <summary>
        /// "iss" (Issuer) Claim
        /// </summary>
        /// <remarks>The "iss" (issuer) claim identifies the principal that issued the
        ///   JWT.  The processing of this claim is generally application specific.
        ///   The "iss" value is a case-sensitive string containing a StringOrURI
        ///   value.  Use of this claim is OPTIONAL.</remarks>
        public string Issuer { get; set; }

        /// <summary>
        /// "sub" (Subject) Claim
        /// </summary>
        /// <remarks> The "sub" (subject) claim identifies the principal that is the
        ///   subject of the JWT.  The claims in a JWT are normally statements
        ///   about the subject.  The subject value MUST either be scoped to be
        ///   locally unique in the context of the issuer or be globally unique.
        ///   The processing of this claim is generally application specific.  The
        ///   "sub" value is a case-sensitive string containing a StringOrURI
        ///   value.  Use of this claim is OPTIONAL.</remarks>
        public string Subject { get; set; }

        /// <summary>
        /// "aud" (Audience) Claim
        /// </summary>
        /// <remarks>The "aud" (audience) claim identifies the recipients that the JWT is
        ///   intended for.  Each principal intended to process the JWT MUST
        ///   identify itself with a value in the audience claim.  If the principal
        ///   processing the claim does not identify itself with a value in the
        ///   "aud" claim when this claim is present, then the JWT MUST be
        ///   rejected.  In the general case, the "aud" value is an array of case-
        ///   sensitive strings, each containing a StringOrURI value.  In the
        ///   special case when the JWT has one audience, the "aud" value MAY be a
        ///   single case-sensitive string containing a StringOrURI value.  The
        ///   interpretation of audience values is generally application specific.
        ///   Use of this claim is OPTIONAL.</remarks>
        public string Audience { get; set; }

        /// <summary>
        /// "nbf" (Not Before) Claim (default is UTC NOW)
        /// </summary>
        /// <remarks>The "nbf" (not before) claim identifies the time before which the JWT
        ///   MUST NOT be accepted for processing.  The processing of the "nbf"
        ///   claim requires that the current date/time MUST be after or equal to
        ///   the not-before date/time listed in the "nbf" claim.  Implementers MAY
        ///   provide for some small leeway, usually no more than a few minutes, to
        ///   account for clock skew.  Its value MUST be a number containing a
        ///   NumericDate value.  Use of this claim is OPTIONAL.</remarks>
        public DateTime NotBefore => DateTime.UtcNow;

        /// <summary>
        /// "iat" (Issued At) Claim (default is UTC NOW)
        /// </summary>
        /// <remarks>The "iat" (issued at) claim identifies the time at which the JWT was
        ///   issued.  This claim can be used to determine the age of the JWT.  Its
        ///   value MUST be a number containing a NumericDate value.  Use of this
        ///   claim is OPTIONAL.</remarks>
        public DateTime IssuedAt => DateTime.UtcNow;

        /// <summary>
        /// Set the timespan the token will be valid for (default is 5 min/300 seconds)
        /// </summary>
        public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(5);

        /// <summary>
        /// "exp" (Expiration Time) Claim (returns IssuedAt + ValidFor)
        /// </summary>
        /// <remarks>The "exp" (expiration time) claim identifies the expiration time on
        ///   or after which the JWT MUST NOT be accepted for processing.  The
        ///   processing of the "exp" claim requires that the current date/time
        ///   MUST be before the expiration date/time listed in the "exp" claim.
        ///   Implementers MAY provide for some small leeway, usually no more than
        ///   a few minutes, to account for clock skew.  Its value MUST be a number
        ///   containing a NumericDate value.  Use of this claim is OPTIONAL.</remarks>
        public DateTime Expiration => IssuedAt.Add(ValidFor);

        /// <summary>
        /// "jti" (JWT ID) Claim (default ID is a GUID)
        /// </summary>
        /// <remarks>The "jti" (JWT ID) claim provides a unique identifier for the JWT.
        ///   The identifier value MUST be assigned in a manner that ensures that
        ///   there is a negligible probability that the same value will be
        ///   accidentally assigned to a different data object; if the application
        ///   uses multiple issuers, collisions MUST be prevented among values
        ///   produced by different issuers as well.  The "jti" claim can be used
        ///   to prevent the JWT from being replayed.  The "jti" value is a case-
        ///   sensitive string.  Use of this claim is OPTIONAL.</remarks>
        public Func<Task<string>> JtiGenerator =>
          () => Task.FromResult(Guid.NewGuid().ToString());

        /// <summary>
        /// The signing key to use when generating tokens.
        /// </summary>
        public SigningCredentials SigningCredentials { get; set; }
    }
}

Add these to appsettings.json file

Take care that the ‘Audience’ value matches the ‘applicationUrl’ under ‘iisSettings’ -> ‘iisExpress’ in your ‘launchSettings.json’ file (under ‘Properties’ when you view your project in the VS Solution Explorer pane).

 "JwtIssuerOptions": {
    "Issuer": "SuperAwesomeTokenServer",
    "Audience": "http://localhost:1783/"
  },

Add these lines to Startup.cs

private const string SecretKey = "needtogetthisfromenvironment";
private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey));

var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions));

// Configure JwtIssuerOptions
services.Configure<JwtIssuerOptions>(options =>
{
  options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)];
  options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)];
  options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256);
});

NET Identity on MySQL Problem

Check the solution here : [Extending IdentityUser using MySql in Core 1.1]

Checking if a user can log in

 private async Task<ClaimsIdentity> GetIdentity(string email, string password)
        {
            var result = await _signInManager.PasswordSignInAsync(email, password, false, lockoutOnFailure: false);
            if (result.Succeeded)
            {
                var user = await _userManager.FindByEmailAsync(email);
                var claims = await _userManager.GetClaimsAsync(user);

                return new ClaimsIdentity(new GenericIdentity(email, "Token"), claims);
            }
        
            // Credentials are invalid, or account doesn't exist
            return null;
        }

GIT : Tokens - Registration - Verification and Login works now ee78860