Starting a new dotnet core project
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