├── src ├── Web.Api.Core │ ├── Interfaces │ │ ├── IUseCaseRequest.cs │ │ ├── IOutputPort.cs │ │ ├── Services │ │ │ └── IJwtFactory.cs │ │ ├── UseCases │ │ │ ├── ILoginUseCase.cs │ │ │ └── IRegisterUserUseCase.cs │ │ ├── IUseCaseRequestHandler.cs │ │ ├── UseCaseResponseMessage.cs │ │ └── Gateways │ │ │ └── Repositories │ │ │ └── IUserRepository.cs │ ├── AssemblyInfo.cs │ ├── Web.Api.Core.csproj │ ├── Dto │ │ ├── Error.cs │ │ ├── Token.cs │ │ ├── GatewayResponses │ │ │ ├── Repositories │ │ │ │ └── CreateUserResponse.cs │ │ │ └── BaseGatewayResponse.cs │ │ ├── UseCaseRequests │ │ │ ├── LoginRequest.cs │ │ │ └── RegisterUserRequest.cs │ │ └── UseCaseResponses │ │ │ ├── LoginResponse.cs │ │ │ └── RegisterUserResponse.cs │ ├── CoreModule.cs │ ├── Domain │ │ └── Entities │ │ │ └── User.cs │ └── UseCases │ │ ├── RegisterUserUseCase.cs │ │ └── LoginUseCase.cs ├── Web.Api │ ├── appsettings.Development.json │ ├── Models │ │ ├── Request │ │ │ ├── LoginRequest.cs │ │ │ └── RegisterUserRequest.cs │ │ └── Validation │ │ │ └── RegisterUserRequestValidator.cs │ ├── Presenters │ │ ├── JsonContentResult.cs │ │ ├── LoginPresenter.cs │ │ └── RegisterUserPresenter.cs │ ├── appsettings.json │ ├── Extensions │ │ └── ResponseExtensions.cs │ ├── Program.cs │ ├── Serialization │ │ └── JsonSerializer.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Web.Api.csproj │ ├── Controllers │ │ ├── AuthController.cs │ │ └── AccountsController.cs │ └── Startup.cs ├── Web.Api.Infrastructure │ ├── appsettings.json │ ├── Data │ │ ├── Entities │ │ │ └── AppUser.cs │ │ ├── EntityFramework │ │ │ ├── DbContextFactory.cs │ │ │ ├── Entities │ │ │ │ └── BaseEntity.cs │ │ │ ├── ApplicationDbContext.cs │ │ │ ├── Repositories │ │ │ │ └── UserRepository.cs │ │ │ └── DesignTimeDbContextFactoryBase.cs │ │ └── Mapping │ │ │ └── Profiles.cs │ ├── Helpers │ │ └── Constants.cs │ ├── InfrastructureModule.cs │ ├── appsettings.Designer.cs │ ├── Web.Api.Infrastructure.csproj │ ├── Auth │ │ ├── JwtIssuerOptions.cs │ │ └── JwtFactory.cs │ └── Migrations │ │ ├── ApplicationDbContextModelSnapshot.cs │ │ ├── 20181003180228_initial.Designer.cs │ │ └── 20181003180228_initial.cs ├── .vscode │ ├── tasks.json │ └── launch.json ├── Tests │ ├── Web.Api.Core.UnitTests │ │ ├── Web.Api.Core.UnitTests.csproj │ │ └── UseCases │ │ │ ├── RegisterUserUseCaseUnitTests.cs │ │ │ └── LoginUseCaseUnitTests.cs │ └── Web.Api.UnitTests │ │ ├── Web.Api.UnitTests.csproj │ │ ├── Presenters │ │ ├── RegisterUserPresenterUnitTests.cs │ │ └── LoginPresenterUnitTests.cs │ │ └── Controllers │ │ └── AccountsControllerUnitTests.cs └── CleanAspNetCoreWebApi.sln ├── README.md ├── LICENSE └── .gitignore /src/Web.Api.Core/Interfaces/IUseCaseRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Core.Interfaces 4 | { 5 | public interface IUseCaseRequest { } 6 | } 7 | -------------------------------------------------------------------------------- /src/Web.Api.Core/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("Web.Api.Infrastructure"), InternalsVisibleTo("Web.Api.Core.UnitTests")] -------------------------------------------------------------------------------- /src/Web.Api/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "Default": "Server=(localdb)\\mssqllocaldb;Database=CleanAspNetCoreWebAPI;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/IOutputPort.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Core.Interfaces 4 | { 5 | public interface IOutputPort 6 | { 7 | void Handle(TUseCaseResponse response); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Web.Api/Models/Request/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Models.Request 4 | { 5 | public class LoginRequest 6 | { 7 | public string UserName { get; set; } 8 | public string Password { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Web.Api.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Web.Api/Presenters/JsonContentResult.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Web.Api.Presenters 4 | { 5 | public sealed class JsonContentResult : ContentResult 6 | { 7 | public JsonContentResult() 8 | { 9 | ContentType = "application/json"; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/Services/IJwtFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Web.Api.Core.Dto; 3 | 4 | namespace Web.Api.Core.Interfaces.Services 5 | { 6 | public interface IJwtFactory 7 | { 8 | Task GenerateEncodedToken(string id, string userName); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/UseCases/ILoginUseCase.cs: -------------------------------------------------------------------------------- 1 | using Web.Api.Core.Dto.UseCaseRequests; 2 | using Web.Api.Core.Dto.UseCaseResponses; 3 | 4 | namespace Web.Api.Core.Interfaces.UseCases 5 | { 6 | public interface ILoginUseCase : IUseCaseRequestHandler 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/UseCases/IRegisterUserUseCase.cs: -------------------------------------------------------------------------------- 1 | using Web.Api.Core.Dto.UseCaseRequests; 2 | using Web.Api.Core.Dto.UseCaseResponses; 3 | 4 | namespace Web.Api.Core.Interfaces.UseCases 5 | { 6 | public interface IRegisterUserUseCase : IUseCaseRequestHandler 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Web.Api/Models/Request/RegisterUserRequest.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Models.Request 4 | { 5 | public class RegisterUserRequest 6 | { 7 | public string FirstName { get; set; } 8 | public string LastName { get; set; } 9 | public string Email { get; set; } 10 | public string UserName { get; set; } 11 | public string Password { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/Error.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Core.Dto 4 | { 5 | public sealed class Error 6 | { 7 | public string Code { get; } 8 | public string Description { get; } 9 | 10 | public Error(string code, string description) 11 | { 12 | Code = code; 13 | Description = description; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/IUseCaseRequestHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace Web.Api.Core.Interfaces 4 | { 5 | public interface IUseCaseRequestHandler where TUseCaseRequest : IUseCaseRequest 6 | { 7 | Task Handle(TUseCaseRequest message, IOutputPort outputPort); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/Web.Api/Web.Api.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/Token.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Core.Dto 4 | { 5 | public sealed class Token 6 | { 7 | public string Id { get; } 8 | public string AuthToken { get; } 9 | public int ExpiresIn { get; } 10 | 11 | public Token(string id, string authToken, int expiresIn) 12 | { 13 | Id = id; 14 | AuthToken = authToken; 15 | ExpiresIn = expiresIn; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/Entities/AppUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace Web.Api.Infrastructure.Data.Entities 4 | { 5 | // Add profile data for application users by adding properties to this class 6 | public class AppUser : IdentityUser 7 | { 8 | // Extended Properties 9 | public string FirstName { get; set; } 10 | public string LastName { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Web.Api/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning" 5 | } 6 | }, 7 | "AllowedHosts": "*", 8 | "ConnectionStrings": { 9 | "Default": "Server=(localdb)\\mssqllocaldb;Database=CleanAspNetCoreWebAPI;Trusted_Connection=True;MultipleActiveResultSets=true" 10 | }, 11 | "JwtIssuerOptions": { 12 | "Issuer": "webApi", 13 | "Audience": "http://localhost:5000/" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/UseCaseResponseMessage.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Web.Api.Core.Interfaces 3 | { 4 | public abstract class UseCaseResponseMessage 5 | { 6 | public bool Success { get; } 7 | public string Message { get; } 8 | 9 | protected UseCaseResponseMessage(bool success = false, string message = null) 10 | { 11 | Success = success; 12 | Message = message; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/GatewayResponses/Repositories/CreateUserResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Web.Api.Core.Dto.GatewayResponses.Repositories 4 | { 5 | public sealed class CreateUserResponse : BaseGatewayResponse 6 | { 7 | public string Id { get; } 8 | public CreateUserResponse(string id, bool success = false, IEnumerable errors = null) : base(success, errors) 9 | { 10 | Id = id; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Helpers/Constants.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace Web.Api.Infrastructure.Helpers 4 | { 5 | public static class Constants 6 | { 7 | public static class Strings 8 | { 9 | public static class JwtClaimIdentifiers 10 | { 11 | public const string Rol = "rol", Id = "id"; 12 | } 13 | 14 | public static class JwtClaims 15 | { 16 | public const string ApiAccess = "api_access"; 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/GatewayResponses/BaseGatewayResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Web.Api.Core.Dto.GatewayResponses 4 | { 5 | public abstract class BaseGatewayResponse 6 | { 7 | public bool Success { get; } 8 | public IEnumerable Errors { get; } 9 | 10 | protected BaseGatewayResponse(bool success=false, IEnumerable errors=null) 11 | { 12 | Success = success; 13 | Errors = errors; 14 | } 15 | } 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/EntityFramework/DbContextFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | 4 | namespace Web.Api.Infrastructure.Data.EntityFramework 5 | { 6 | public class DbContextFactory : DesignTimeDbContextFactoryBase 7 | { 8 | protected override ApplicationDbContext CreateNewInstance(DbContextOptions options) 9 | { 10 | return new ApplicationDbContext(options); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web.Api/Extensions/ResponseExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Web.Api.Extensions 4 | { 5 | public static class ResponseExtensions 6 | { 7 | public static void AddApplicationError(this HttpResponse response, string message) 8 | { 9 | response.Headers.Add("Application-Error", message); 10 | // CORS 11 | response.Headers.Add("access-control-expose-headers", "Application-Error"); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Web.Api.Core/CoreModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Web.Api.Core.Interfaces.UseCases; 3 | using Web.Api.Core.UseCases; 4 | 5 | namespace Web.Api.Core 6 | { 7 | public class CoreModule : Module 8 | { 9 | protected override void Load(ContainerBuilder builder) 10 | { 11 | builder.RegisterType().As().InstancePerLifetimeScope(); 12 | builder.RegisterType().As().InstancePerLifetimeScope(); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/UseCaseRequests/LoginRequest.cs: -------------------------------------------------------------------------------- 1 | using Web.Api.Core.Dto.UseCaseResponses; 2 | using Web.Api.Core.Interfaces; 3 | 4 | namespace Web.Api.Core.Dto.UseCaseRequests 5 | { 6 | public class LoginRequest : IUseCaseRequest 7 | { 8 | public string UserName { get; } 9 | public string Password { get; } 10 | 11 | public LoginRequest(string userName, string password) 12 | { 13 | UserName = userName; 14 | Password = password; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Interfaces/Gateways/Repositories/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Web.Api.Core.Domain.Entities; 3 | using Web.Api.Core.Dto.GatewayResponses.Repositories; 4 | 5 | namespace Web.Api.Core.Interfaces.Gateways.Repositories 6 | { 7 | public interface IUserRepository 8 | { 9 | Task Create(User user, string password); 10 | Task FindByName(string userName); 11 | Task CheckPassword(User user, string password); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/EntityFramework/Entities/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace Web.Api.Infrastructure.Data.EntityFramework.Entities 6 | { 7 | public class BaseEntity 8 | { 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | [Key] 11 | public int Id { get; set; } 12 | public DateTime Created { get; set; } 13 | public DateTime Modified { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Web.Api/Models/Validation/RegisterUserRequestValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Web.Api.Models.Request; 3 | 4 | namespace Web.Api.Models.Validation 5 | { 6 | public class RegisterUserRequestValidator : AbstractValidator 7 | { 8 | public RegisterUserRequestValidator() 9 | { 10 | RuleFor(x => x.FirstName).Length(2, 30); 11 | RuleFor(x => x.LastName).Length(2, 30); 12 | RuleFor(x => x.UserName).Length(5, 255); 13 | RuleFor(x => x.Password).Length(6, 15); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/InfrastructureModule.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Web.Api.Core.Interfaces.Gateways.Repositories; 3 | using Web.Api.Core.Interfaces.Services; 4 | using Web.Api.Infrastructure.Auth; 5 | using Web.Api.Infrastructure.Data.EntityFramework.Repositories; 6 | namespace Web.Api.Infrastructure 7 | { 8 | public class InfrastructureModule : Module 9 | { 10 | protected override void Load(ContainerBuilder builder) 11 | { 12 | builder.RegisterType().As().InstancePerLifetimeScope(); 13 | builder.RegisterType().As().SingleInstance(); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/Mapping/Profiles.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Web.Api.Core.Domain.Entities; 3 | using Web.Api.Infrastructure.Data.Entities; 4 | 5 | 6 | namespace Web.Api.Infrastructure.Data.Mapping 7 | { 8 | public class DataProfile : Profile 9 | { 10 | public DataProfile() 11 | { 12 | CreateMap().ConstructUsing(u => new AppUser {Id=u.Id, FirstName = u.FirstName, LastName = u.LastName, UserName = u.UserName, PasswordHash = u.PasswordHash}); 13 | CreateMap().ConstructUsing(au => new User(au.FirstName, au.LastName, au.Email, au.UserName, au.Id, au.PasswordHash)); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/UseCaseResponses/LoginResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Web.Api.Core.Interfaces; 3 | 4 | namespace Web.Api.Core.Dto.UseCaseResponses 5 | { 6 | public class LoginResponse : UseCaseResponseMessage 7 | { 8 | public Token Token { get; } 9 | public IEnumerable Errors { get; } 10 | 11 | public LoginResponse(IEnumerable errors, bool success = false, string message = null) : base(success, message) 12 | { 13 | Errors = errors; 14 | } 15 | 16 | public LoginResponse(Token token, bool success = false, string message = null) : base(success, message) 17 | { 18 | Token = token; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Tests/Web.Api.Core.UnitTests/Web.Api.Core.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Web.Api/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Web.Api 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/UseCaseResponses/RegisterUserResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Web.Api.Core.Interfaces; 3 | 4 | namespace Web.Api.Core.Dto.UseCaseResponses 5 | { 6 | public class RegisterUserResponse : UseCaseResponseMessage 7 | { 8 | public string Id { get; } 9 | public IEnumerable Errors { get; } 10 | 11 | public RegisterUserResponse(IEnumerable errors, bool success=false, string message=null) : base(success, message) 12 | { 13 | Errors = errors; 14 | } 15 | 16 | public RegisterUserResponse(string id, bool success = false, string message = null) : base(success, message) 17 | { 18 | Id = id; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Tests/Web.Api.UnitTests/Web.Api.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Web.Api/Serialization/JsonSerializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | 4 | namespace Web.Api.Serialization 5 | { 6 | public sealed class JsonSerializer 7 | { 8 | private static readonly JsonSerializerSettings Settings = new JsonSerializerSettings 9 | { 10 | ContractResolver = new JsonContractResolver(), 11 | NullValueHandling = NullValueHandling.Ignore 12 | }; 13 | 14 | public static string SerializeObject(object o) 15 | { 16 | return JsonConvert.SerializeObject(o, Formatting.Indented, Settings); 17 | } 18 | 19 | public sealed class JsonContractResolver : CamelCasePropertyNamesContractResolver 20 | { 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Domain/Entities/User.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace Web.Api.Core.Domain.Entities 3 | { 4 | public class User 5 | { 6 | public string Id { get; } 7 | public string FirstName { get; } 8 | public string LastName { get; } 9 | public string Email { get; } 10 | public string UserName { get; } 11 | public string PasswordHash { get; } 12 | 13 | internal User(string firstName, string lastName, string email, string userName, string id=null,string passwordHash=null) 14 | { 15 | Id = id; 16 | FirstName = firstName; 17 | LastName = lastName; 18 | Email = email; 19 | UserName = userName; 20 | PasswordHash = passwordHash; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Web.Api.Core/Dto/UseCaseRequests/RegisterUserRequest.cs: -------------------------------------------------------------------------------- 1 | using Web.Api.Core.Dto.UseCaseResponses; 2 | using Web.Api.Core.Interfaces; 3 | 4 | namespace Web.Api.Core.Dto.UseCaseRequests 5 | { 6 | public class RegisterUserRequest : IUseCaseRequest 7 | { 8 | public string FirstName { get; } 9 | public string LastName { get; } 10 | public string Email { get; } 11 | public string UserName { get; } 12 | public string Password { get; } 13 | 14 | public RegisterUserRequest(string firstName, string lastName, string email, string userName, string password) 15 | { 16 | FirstName = firstName; 17 | LastName = lastName; 18 | Email = email; 19 | UserName = userName; 20 | Password = password; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Web.Api/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:5000/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Web.Api": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "launchUrl": "swagger", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "http://localhost:5000/" 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Web.Api/Presenters/LoginPresenter.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Web.Api.Core.Dto.UseCaseResponses; 3 | using Web.Api.Core.Interfaces; 4 | using Web.Api.Serialization; 5 | 6 | namespace Web.Api.Presenters 7 | { 8 | public sealed class LoginPresenter : IOutputPort 9 | { 10 | public JsonContentResult ContentResult { get; } 11 | 12 | public LoginPresenter() 13 | { 14 | ContentResult = new JsonContentResult(); 15 | } 16 | 17 | public void Handle(LoginResponse response) 18 | { 19 | ContentResult.StatusCode = (int)(response.Success ? HttpStatusCode.OK : HttpStatusCode.Unauthorized); 20 | ContentResult.Content = response.Success ? JsonSerializer.SerializeObject(response.Token) : JsonSerializer.SerializeObject(response.Errors); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Web.Api/Presenters/RegisterUserPresenter.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Web.Api.Core.Dto.UseCaseResponses; 3 | using Web.Api.Core.Interfaces; 4 | using Web.Api.Serialization; 5 | 6 | namespace Web.Api.Presenters 7 | { 8 | public sealed class RegisterUserPresenter : IOutputPort 9 | { 10 | public JsonContentResult ContentResult { get; } 11 | 12 | public RegisterUserPresenter() 13 | { 14 | ContentResult = new JsonContentResult(); 15 | } 16 | 17 | public void Handle(RegisterUserResponse response) 18 | { 19 | ContentResult.StatusCode = (int)(response.Success ? HttpStatusCode.OK : HttpStatusCode.BadRequest); 20 | ContentResult.Content = JsonSerializer.SerializeObject(response); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Web.Api/Web.Api.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleanAspNetCoreWebApi 2 | Starter project for creating APIs built on ASP.NET Core using clean architecture based on [this blog post](https://fullstackmark.com/post/18/building-aspnet-core-web-apis-with-clean-architecture). 3 | 4 | # Setup 5 | - Uses Sql Server Express LocalDB (If using Visual Studio install it under Individual Components in the Visual Studio installer or install separately using [this link](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-2016-express-localdb?view=sql-server-2017). 6 | - Apply database migrations to create the db. From a command line within the *Web.Api.Infrastructure* project folder use the dotnet CLI to run : Web.Api.Infrastructure>**dotnet ef database update** 7 | 8 | # Visual Studio 9 | Simply open the solution file CleanAspNetCoreWebAPI.sln and build/run. 10 | 11 | # Visual Studio Code 12 | Open the src folder and F5 to build/run. 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Mark Macneil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Web.Api/Controllers/AuthController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Web.Api.Core.Dto.UseCaseRequests; 4 | using Web.Api.Core.Interfaces.UseCases; 5 | using Web.Api.Presenters; 6 | 7 | namespace Web.Api.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | public class AuthController : ControllerBase 12 | { 13 | private readonly ILoginUseCase _loginUseCase; 14 | private readonly LoginPresenter _loginPresenter; 15 | 16 | public AuthController(ILoginUseCase loginUseCase, LoginPresenter loginPresenter) 17 | { 18 | _loginUseCase = loginUseCase; 19 | _loginPresenter = loginPresenter; 20 | } 21 | 22 | // POST api/auth/login 23 | [HttpPost("login")] 24 | public async Task Login([FromBody] Models.Request.LoginRequest request) 25 | { 26 | if (!ModelState.IsValid) 27 | { // re-render the view when validation failed. 28 | return BadRequest(ModelState); 29 | } 30 | await _loginUseCase.Handle(new LoginRequest(request.UserName, request.Password), _loginPresenter); 31 | return _loginPresenter.ContentResult; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/appsettings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace Infrastructure { 12 | 13 | 14 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 15 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.8.0.0")] 16 | internal sealed partial class appsettings : global::System.Configuration.ApplicationSettingsBase { 17 | 18 | private static appsettings defaultInstance = ((appsettings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new appsettings()))); 19 | 20 | public static appsettings Default { 21 | get { 22 | return defaultInstance; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Web.Api.Core/UseCases/RegisterUserUseCase.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Web.Api.Core.Domain.Entities; 4 | using Web.Api.Core.Dto.UseCaseRequests; 5 | using Web.Api.Core.Dto.UseCaseResponses; 6 | using Web.Api.Core.Interfaces; 7 | using Web.Api.Core.Interfaces.Gateways.Repositories; 8 | using Web.Api.Core.Interfaces.UseCases; 9 | 10 | namespace Web.Api.Core.UseCases 11 | { 12 | public sealed class RegisterUserUseCase : IRegisterUserUseCase 13 | { 14 | private readonly IUserRepository _userRepository; 15 | 16 | public RegisterUserUseCase(IUserRepository userRepository) 17 | { 18 | _userRepository = userRepository; 19 | } 20 | 21 | public async Task Handle(RegisterUserRequest message, IOutputPort outputPort) 22 | { 23 | var response = await _userRepository.Create(new User(message.FirstName, message.LastName,message.Email, message.UserName), message.Password); 24 | outputPort.Handle(response.Success ? new RegisterUserResponse(response.Id, true) : new RegisterUserResponse(response.Errors.Select(e => e.Description))); 25 | return response.Success; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Web.Api/Controllers/AccountsController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Web.Api.Core.Dto.UseCaseRequests; 4 | using Web.Api.Core.Interfaces.UseCases; 5 | using Web.Api.Presenters; 6 | 7 | namespace Web.Api.Controllers 8 | { 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | public class AccountsController : ControllerBase 12 | { 13 | private readonly IRegisterUserUseCase _registerUserUseCase; 14 | private readonly RegisterUserPresenter _registerUserPresenter; 15 | 16 | public AccountsController(IRegisterUserUseCase registerUserUseCase, RegisterUserPresenter registerUserPresenter) 17 | { 18 | _registerUserUseCase = registerUserUseCase; 19 | _registerUserPresenter = registerUserPresenter; 20 | } 21 | 22 | // POST api/accounts 23 | [HttpPost] 24 | public async Task Post([FromBody] Models.Request.RegisterUserRequest request) 25 | { 26 | if (!ModelState.IsValid) 27 | { // re-render the view when validation failed. 28 | return BadRequest(ModelState); 29 | } 30 | await _registerUserUseCase.Handle(new RegisterUserRequest(request.FirstName,request.LastName,request.Email, request.UserName,request.Password), _registerUserPresenter); 31 | return _registerUserPresenter.ContentResult; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/EntityFramework/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore; 6 | using Web.Api.Infrastructure.Data.Entities; 7 | using Web.Api.Infrastructure.Data.EntityFramework.Entities; 8 | 9 | 10 | namespace Web.Api.Infrastructure.Data.EntityFramework 11 | { 12 | public class ApplicationDbContext : IdentityDbContext 13 | { 14 | public ApplicationDbContext(DbContextOptions options) : base(options) 15 | { 16 | } 17 | 18 | public override int SaveChanges() 19 | { 20 | AddAuitInfo(); 21 | return base.SaveChanges(); 22 | } 23 | 24 | public async Task SaveChangesAsync() 25 | { 26 | AddAuitInfo(); 27 | return await base.SaveChangesAsync(); 28 | } 29 | 30 | private void AddAuitInfo() 31 | { 32 | var entries = ChangeTracker.Entries().Where(x => x.Entity is BaseEntity && (x.State == EntityState.Added || x.State == EntityState.Modified)); 33 | foreach (var entry in entries) 34 | { 35 | if (entry.State == EntityState.Added) 36 | { 37 | ((BaseEntity)entry.Entity).Created = DateTime.UtcNow; 38 | } 39 | ((BaseEntity)entry.Entity).Modified = DateTime.UtcNow; 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/EntityFramework/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using AutoMapper; 4 | using Microsoft.AspNetCore.Identity; 5 | using Web.Api.Core.Domain.Entities; 6 | using Web.Api.Core.Dto; 7 | using Web.Api.Core.Dto.GatewayResponses.Repositories; 8 | using Web.Api.Core.Interfaces.Gateways.Repositories; 9 | using Web.Api.Infrastructure.Data.Entities; 10 | 11 | 12 | namespace Web.Api.Infrastructure.Data.EntityFramework.Repositories 13 | { 14 | internal sealed class UserRepository : IUserRepository 15 | { 16 | private readonly UserManager _userManager; 17 | private readonly IMapper _mapper; 18 | 19 | public UserRepository(UserManager userManager, IMapper mapper) 20 | { 21 | _userManager = userManager; 22 | _mapper = mapper; 23 | } 24 | 25 | public async Task Create(User user, string password) 26 | { 27 | var appUser = _mapper.Map(user); 28 | var identityResult = await _userManager.CreateAsync(appUser, password); 29 | return new CreateUserResponse(appUser.Id,identityResult.Succeeded, identityResult.Succeeded ? null : identityResult.Errors.Select(e => new Error(e.Code, e.Description))); 30 | } 31 | 32 | public async Task FindByName(string userName) 33 | { 34 | return _mapper.Map(await _userManager.FindByNameAsync(userName)); 35 | } 36 | 37 | public async Task CheckPassword(User user, string password) 38 | { 39 | return await _userManager.CheckPasswordAsync(_mapper.Map(user), password); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Tests/Web.Api.Core.UnitTests/UseCases/RegisterUserUseCaseUnitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Moq; 3 | using Web.Api.Core.Domain.Entities; 4 | using Web.Api.Core.Dto.GatewayResponses.Repositories; 5 | using Web.Api.Core.Dto.UseCaseRequests; 6 | using Web.Api.Core.Dto.UseCaseResponses; 7 | using Web.Api.Core.Interfaces; 8 | using Web.Api.Core.Interfaces.Gateways.Repositories; 9 | using Web.Api.Core.UseCases; 10 | using Xunit; 11 | 12 | namespace Web.Api.Core.UnitTests.UseCases 13 | { 14 | public class RegisterUserUseCaseUnitTests 15 | { 16 | 17 | [Fact] 18 | public async void Can_Register_User() 19 | { 20 | // arrange 21 | 22 | // 1. We need to store the user data somehow 23 | var mockUserRepository = new Mock(); 24 | mockUserRepository 25 | .Setup(repo => repo.Create(It.IsAny(), It.IsAny())) 26 | .Returns(Task.FromResult(new CreateUserResponse("", true))); 27 | 28 | // 2. The use case and star of this test 29 | var useCase = new RegisterUserUseCase(mockUserRepository.Object); 30 | 31 | // 3. The output port is the mechanism to pass response data from the use case to a Presenter 32 | // for final preparation to deliver back to the UI/web page/api response etc. 33 | var mockOutputPort = new Mock>(); 34 | mockOutputPort.Setup(outputPort => outputPort.Handle(It.IsAny())); 35 | 36 | // act 37 | 38 | // 4. We need a request model to carry data into the use case from the upper layer (UI, Controller etc.) 39 | var response = await useCase.Handle(new RegisterUserRequest("firstName", "lastName", "me@domain.com", "userName", "password"), mockOutputPort.Object); 40 | 41 | // assert 42 | Assert.True(response); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/Web.Api/bin/Debug/netcoreapp2.1/Web.Api.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/Web.Api", 16 | "stopAtEntry": false, 17 | "internalConsoleOptions": "openOnSessionStart", 18 | "launchBrowser": { 19 | "enabled": true, 20 | "args": "http://localhost:5000/swagger", 21 | "windows": { 22 | "command": "cmd.exe", 23 | "args": "/C start http://localhost:5000/swagger" 24 | }, 25 | "osx": { 26 | "command": "open" 27 | }, 28 | "linux": { 29 | "command": "xdg-open" 30 | } 31 | }, 32 | "env": { 33 | "ASPNETCORE_ENVIRONMENT": "Development" 34 | }, 35 | "sourceFileMap": { 36 | "/Views": "${workspaceFolder}/Views" 37 | } 38 | }, 39 | { 40 | "name": ".NET Core Attach", 41 | "type": "coreclr", 42 | "request": "attach", 43 | "processId": "${command:pickProcess}" 44 | } 45 | ,] 46 | } -------------------------------------------------------------------------------- /src/Tests/Web.Api.UnitTests/Presenters/RegisterUserPresenterUnitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using Newtonsoft.Json; 3 | using Web.Api.Core.Dto.UseCaseResponses; 4 | using Web.Api.Presenters; 5 | using Xunit; 6 | 7 | namespace Web.Api.UnitTests.Presenters 8 | { 9 | public class RegisterUserPresenterUnitTests 10 | { 11 | [Fact] 12 | public void Contains_Ok_Status_Code_When_Use_Case_Succeeds() 13 | { 14 | // arrange 15 | var presenter = new RegisterUserPresenter(); 16 | 17 | // act 18 | presenter.Handle(new RegisterUserResponse("", true)); 19 | 20 | // assert 21 | Assert.Equal((int)HttpStatusCode.OK,presenter.ContentResult.StatusCode); 22 | } 23 | 24 | [Fact] 25 | public void Contains_Id_When_Use_Case_Succeeds() 26 | { 27 | // arrange 28 | var presenter = new RegisterUserPresenter(); 29 | 30 | // act 31 | presenter.Handle(new RegisterUserResponse("1234", true)); 32 | 33 | // assert 34 | dynamic data = JsonConvert.DeserializeObject(presenter.ContentResult.Content); 35 | Assert.True(data.success.Value); 36 | Assert.Equal("1234",data.id.Value); 37 | } 38 | 39 | [Fact] 40 | public void Contains_Errors_When_Use_Case_Fails() 41 | { 42 | // arrange 43 | var presenter = new RegisterUserPresenter(); 44 | 45 | // act 46 | presenter.Handle(new RegisterUserResponse(new [] {"missing first name"})); 47 | 48 | // assert 49 | dynamic data = JsonConvert.DeserializeObject(presenter.ContentResult.Content); 50 | Assert.False(data.success.Value); 51 | Assert.Equal("missing first name", data.errors.First.Value); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Web.Api.Core/UseCases/LoginUseCase.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Web.Api.Core.Dto; 3 | using Web.Api.Core.Dto.UseCaseRequests; 4 | using Web.Api.Core.Dto.UseCaseResponses; 5 | using Web.Api.Core.Interfaces; 6 | using Web.Api.Core.Interfaces.Gateways.Repositories; 7 | using Web.Api.Core.Interfaces.Services; 8 | using Web.Api.Core.Interfaces.UseCases; 9 | 10 | namespace Web.Api.Core.UseCases 11 | { 12 | public sealed class LoginUseCase : ILoginUseCase 13 | { 14 | private readonly IUserRepository _userRepository; 15 | private readonly IJwtFactory _jwtFactory; 16 | 17 | public LoginUseCase(IUserRepository userRepository, IJwtFactory jwtFactory) 18 | { 19 | _userRepository = userRepository; 20 | _jwtFactory = jwtFactory; 21 | } 22 | 23 | public async Task Handle(LoginRequest message, IOutputPort outputPort) 24 | { 25 | if (!string.IsNullOrEmpty(message.UserName) && !string.IsNullOrEmpty(message.Password)) 26 | { 27 | // confirm we have a user with the given name 28 | var user = await _userRepository.FindByName(message.UserName); 29 | if (user != null) 30 | { 31 | // validate password 32 | if (await _userRepository.CheckPassword(user, message.Password)) 33 | { 34 | // generate token 35 | outputPort.Handle(new LoginResponse(await _jwtFactory.GenerateEncodedToken(user.Id, user.UserName),true)); 36 | return true; 37 | } 38 | } 39 | } 40 | outputPort.Handle(new LoginResponse(new[] { new Error("login_failure", "Invalid username or password.") })); 41 | return false; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tests/Web.Api.UnitTests/Presenters/LoginPresenterUnitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Net; 4 | using Newtonsoft.Json; 5 | using Web.Api.Core.Dto; 6 | using Web.Api.Core.Dto.UseCaseResponses; 7 | using Web.Api.Presenters; 8 | using Xunit; 9 | 10 | namespace Web.Api.UnitTests.Presenters 11 | { 12 | public class LoginPresenterUnitTests 13 | { 14 | [Fact] 15 | public void Contains_Ok_Status_Code_When_Use_Case_Succeeds() 16 | { 17 | // arrange 18 | var presenter = new LoginPresenter(); 19 | 20 | // act 21 | presenter.Handle(new LoginResponse(new Token("", "", 0), true)); 22 | 23 | // assert 24 | Assert.Equal((int)HttpStatusCode.OK, presenter.ContentResult.StatusCode); 25 | } 26 | 27 | [Fact] 28 | public void Contains_Token_When_Use_Case_Succeeds() 29 | { 30 | // arrange 31 | const string authToken = "777888AAABBB"; 32 | var presenter = new LoginPresenter(); 33 | 34 | // act 35 | presenter.Handle(new LoginResponse(new Token("1", authToken, 0), true)); 36 | 37 | // assert 38 | dynamic data = JsonConvert.DeserializeObject(presenter.ContentResult.Content); 39 | Assert.Equal(authToken, data.authtoken.Value); 40 | } 41 | 42 | [Fact] 43 | public void Contains_Errors_When_Use_Case_Fails() 44 | { 45 | // arrange 46 | var presenter = new LoginPresenter(); 47 | 48 | // act 49 | presenter.Handle(new LoginResponse(new[] { new Error("", "Invalid username/password") })); 50 | 51 | // assert 52 | var data = JsonConvert.DeserializeObject>(presenter.ContentResult.Content); 53 | Assert.Equal((int)HttpStatusCode.Unauthorized, presenter.ContentResult.StatusCode); 54 | Assert.Equal("Invalid username/password", data.First().Description); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Tests/Web.Api.UnitTests/Controllers/AccountsControllerUnitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Moq; 5 | using Web.Api.Controllers; 6 | using Web.Api.Core.Domain.Entities; 7 | using Web.Api.Core.Dto.GatewayResponses.Repositories; 8 | using Web.Api.Core.Interfaces.Gateways.Repositories; 9 | using Web.Api.Core.UseCases; 10 | using Web.Api.Presenters; 11 | using Xunit; 12 | 13 | namespace Web.Api.UnitTests.Controllers 14 | { 15 | public class AccountsControllerUnitTests 16 | { 17 | [Fact] 18 | public async void Post_Returns_Ok_When_Use_Case_Succeeds() 19 | { 20 | // arrange 21 | var mockUserRepository = new Mock(); 22 | mockUserRepository 23 | .Setup(repo => repo.Create(It.IsAny(), It.IsAny())) 24 | .Returns(Task.FromResult(new CreateUserResponse("",true))); 25 | 26 | // fakes 27 | var outputPort = new RegisterUserPresenter(); 28 | var useCase = new RegisterUserUseCase(mockUserRepository.Object); 29 | 30 | var controller = new AccountsController(useCase, outputPort); 31 | 32 | // act 33 | var result = await controller.Post(new Models.Request.RegisterUserRequest()); 34 | 35 | // assert 36 | var statusCode = ((ContentResult) result).StatusCode; 37 | Assert.True(statusCode.HasValue && statusCode.Value == (int) HttpStatusCode.OK); 38 | } 39 | 40 | [Fact] 41 | public async void Post_Returns_Bad_Request_When_Model_Validation_Fails() 42 | { 43 | // arrange 44 | var controller = new AccountsController(null,null); 45 | controller.ModelState.AddModelError("FirstName", "Required"); 46 | 47 | // act 48 | var result = await controller.Post(null); 49 | 50 | // assert 51 | var badRequestResult = Assert.IsType(result); 52 | Assert.IsType(badRequestResult.Value); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Web.Api.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | System 25 | 26 | 27 | 28 | 29 | 30 | True 31 | True 32 | appsettings.json 33 | 34 | 35 | 36 | 37 | 38 | SettingsSingleFileGenerator 39 | appsettings.Designer.cs 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Auth/JwtIssuerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.IdentityModel.Tokens; 4 | 5 | 6 | namespace Web.Api.Infrastructure.Auth 7 | { 8 | public class JwtIssuerOptions 9 | { 10 | /// 11 | /// 4.1.1. "iss" (Issuer) Claim - The "iss" (issuer) claim identifies the principal that issued the JWT. 12 | /// 13 | public string Issuer { get; set; } 14 | 15 | /// 16 | /// 4.1.2. "sub" (Subject) Claim - The "sub" (subject) claim identifies the principal that is the subject of the JWT. 17 | /// 18 | public string Subject { get; set; } 19 | 20 | /// 21 | /// 4.1.3. "aud" (Audience) Claim - The "aud" (audience) claim identifies the recipients that the JWT is intended for. 22 | /// 23 | public string Audience { get; set; } 24 | 25 | /// 26 | /// 4.1.4. "exp" (Expiration Time) Claim - The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing. 27 | /// 28 | public DateTime Expiration => IssuedAt.Add(ValidFor); 29 | 30 | /// 31 | /// 4.1.5. "nbf" (Not Before) Claim - The "nbf" (not before) claim identifies the time before which the JWT MUST NOT be accepted for processing. 32 | /// 33 | public DateTime NotBefore => DateTime.UtcNow; 34 | 35 | /// 36 | /// 4.1.6. "iat" (Issued At) Claim - The "iat" (issued at) claim identifies the time at which the JWT was issued. 37 | /// 38 | public DateTime IssuedAt => DateTime.UtcNow; 39 | 40 | /// 41 | /// Set the timespan the token will be valid for (default is 120 min) 42 | /// 43 | public TimeSpan ValidFor { get; set; } = TimeSpan.FromMinutes(120); 44 | 45 | 46 | 47 | /// 48 | /// "jti" (JWT ID) Claim (default ID is a GUID) 49 | /// 50 | public Func> JtiGenerator => 51 | () => Task.FromResult(Guid.NewGuid().ToString()); 52 | 53 | /// 54 | /// The signing key to use when generating tokens. 55 | /// 56 | public SigningCredentials SigningCredentials { get; set; } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Data/EntityFramework/DesignTimeDbContextFactoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Design; 5 | using Microsoft.Extensions.Configuration; 6 | 7 | namespace Web.Api.Infrastructure.Data.EntityFramework 8 | { 9 | public abstract class DesignTimeDbContextFactoryBase : 10 | IDesignTimeDbContextFactory where TContext : DbContext 11 | { 12 | public TContext CreateDbContext(string[] args) 13 | { 14 | return Create( 15 | Directory.GetCurrentDirectory(), 16 | Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")); 17 | } 18 | protected abstract TContext CreateNewInstance( 19 | DbContextOptions options); 20 | 21 | public TContext Create() 22 | { 23 | var environmentName = 24 | Environment.GetEnvironmentVariable( 25 | "ASPNETCORE_ENVIRONMENT"); 26 | 27 | var basePath = AppContext.BaseDirectory; 28 | 29 | return Create(basePath, environmentName); 30 | } 31 | 32 | private TContext Create(string basePath, string environmentName) 33 | { 34 | var builder = new ConfigurationBuilder() 35 | .SetBasePath(basePath) 36 | .AddJsonFile("appsettings.json") 37 | .AddJsonFile($"appsettings.{environmentName}.json", true) 38 | .AddEnvironmentVariables(); 39 | 40 | var config = builder.Build(); 41 | 42 | var connstr = config.GetConnectionString("Default"); 43 | 44 | if (string.IsNullOrWhiteSpace(connstr)) 45 | { 46 | throw new InvalidOperationException( 47 | "Could not find a connection string named 'Default'."); 48 | } 49 | return Create(connstr); 50 | } 51 | 52 | private TContext Create(string connectionString) 53 | { 54 | if (string.IsNullOrEmpty(connectionString)) 55 | throw new ArgumentException( 56 | $"{nameof(connectionString)} is null or empty.", 57 | nameof(connectionString)); 58 | 59 | var optionsBuilder = new DbContextOptionsBuilder(); 60 | 61 | Console.WriteLine("DesignTimeDbContextFactory.Create(string): Connection string: {0}", connectionString); 62 | 63 | optionsBuilder.UseSqlServer(connectionString); 64 | 65 | var options = optionsBuilder.Options; 66 | return CreateNewInstance(options); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/CleanAspNetCoreWebApi.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web.Api", "Web.Api\Web.Api.csproj", "{39CC82DB-F9FA-405D-914B-DC491F7408DB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web.Api.Infrastructure", "Web.Api.Infrastructure\Web.Api.Infrastructure.csproj", "{74275ABC-7ECB-4D67-AE1A-1BDBBE9D396E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web.Api.Core", "Web.Api.Core\Web.Api.Core.csproj", "{E1FB3417-036E-485B-ABF1-6ECC3124E6F9}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{DC62B07E-986B-4266-B540-1BFB3A022EA3}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web.Api.Core.UnitTests", "Tests\Web.Api.Core.UnitTests\Web.Api.Core.UnitTests.csproj", "{DEFF157F-C17E-4164-8FF5-581B373C397A}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web.Api.UnitTests", "Tests\Web.Api.UnitTests\Web.Api.UnitTests.csproj", "{92C5C72D-8537-41A7-B5CE-52D47DEA4E86}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {39CC82DB-F9FA-405D-914B-DC491F7408DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {39CC82DB-F9FA-405D-914B-DC491F7408DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {39CC82DB-F9FA-405D-914B-DC491F7408DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {39CC82DB-F9FA-405D-914B-DC491F7408DB}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {74275ABC-7ECB-4D67-AE1A-1BDBBE9D396E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {74275ABC-7ECB-4D67-AE1A-1BDBBE9D396E}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {74275ABC-7ECB-4D67-AE1A-1BDBBE9D396E}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {74275ABC-7ECB-4D67-AE1A-1BDBBE9D396E}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {E1FB3417-036E-485B-ABF1-6ECC3124E6F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {E1FB3417-036E-485B-ABF1-6ECC3124E6F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {E1FB3417-036E-485B-ABF1-6ECC3124E6F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {E1FB3417-036E-485B-ABF1-6ECC3124E6F9}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {DEFF157F-C17E-4164-8FF5-581B373C397A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {DEFF157F-C17E-4164-8FF5-581B373C397A}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {DEFF157F-C17E-4164-8FF5-581B373C397A}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {DEFF157F-C17E-4164-8FF5-581B373C397A}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {92C5C72D-8537-41A7-B5CE-52D47DEA4E86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {92C5C72D-8537-41A7-B5CE-52D47DEA4E86}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {92C5C72D-8537-41A7-B5CE-52D47DEA4E86}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {92C5C72D-8537-41A7-B5CE-52D47DEA4E86}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {DEFF157F-C17E-4164-8FF5-581B373C397A} = {DC62B07E-986B-4266-B540-1BFB3A022EA3} 50 | {92C5C72D-8537-41A7-B5CE-52D47DEA4E86} = {DC62B07E-986B-4266-B540-1BFB3A022EA3} 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {93E35E30-C940-4DC3-A97E-420198B4B929} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Auth/JwtFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Linq; 4 | using System.Security.Claims; 5 | using System.Security.Principal; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Options; 8 | using Web.Api.Core.Dto; 9 | using Web.Api.Core.Interfaces.Services; 10 | 11 | namespace Web.Api.Infrastructure.Auth 12 | { 13 | public class JwtFactory : IJwtFactory 14 | { 15 | private readonly JwtIssuerOptions _jwtOptions; 16 | 17 | public JwtFactory(IOptions jwtOptions) 18 | { 19 | _jwtOptions = jwtOptions.Value; 20 | ThrowIfInvalidOptions(_jwtOptions); 21 | } 22 | 23 | public async Task GenerateEncodedToken(string id, string userName) 24 | { 25 | var identity = GenerateClaimsIdentity(id, userName); 26 | 27 | var claims = new[] 28 | { 29 | new Claim(JwtRegisteredClaimNames.Sub, userName), 30 | new Claim(JwtRegisteredClaimNames.Jti, await _jwtOptions.JtiGenerator()), 31 | new Claim(JwtRegisteredClaimNames.Iat, ToUnixEpochDate(_jwtOptions.IssuedAt).ToString(), ClaimValueTypes.Integer64), 32 | identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Rol), 33 | identity.FindFirst(Helpers.Constants.Strings.JwtClaimIdentifiers.Id) 34 | }; 35 | 36 | // Create the JWT security token and encode it. 37 | var jwt = new JwtSecurityToken( 38 | _jwtOptions.Issuer, 39 | _jwtOptions.Audience, 40 | claims, 41 | _jwtOptions.NotBefore, 42 | _jwtOptions.Expiration, 43 | _jwtOptions.SigningCredentials); 44 | 45 | var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt); 46 | return new Token(identity.Claims.Single(c => c.Type == "id").Value, encodedJwt, (int)_jwtOptions.ValidFor.TotalSeconds); 47 | } 48 | 49 | private static ClaimsIdentity GenerateClaimsIdentity(string id, string userName) 50 | { 51 | return new ClaimsIdentity(new GenericIdentity(userName, "Token"), new[] 52 | { 53 | new Claim(Helpers.Constants.Strings.JwtClaimIdentifiers.Id, id), 54 | new Claim(Helpers.Constants.Strings.JwtClaimIdentifiers.Rol, Helpers.Constants.Strings.JwtClaims.ApiAccess) 55 | }); 56 | } 57 | 58 | /// Date converted to seconds since Unix epoch (Jan 1, 1970, midnight UTC). 59 | private static long ToUnixEpochDate(DateTime date) 60 | => (long)Math.Round((date.ToUniversalTime() - 61 | new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)) 62 | .TotalSeconds); 63 | 64 | private static void ThrowIfInvalidOptions(JwtIssuerOptions options) 65 | { 66 | if (options == null) throw new ArgumentNullException(nameof(options)); 67 | 68 | if (options.ValidFor <= TimeSpan.Zero) 69 | { 70 | throw new ArgumentException("Must be a non-zero TimeSpan.", nameof(JwtIssuerOptions.ValidFor)); 71 | } 72 | 73 | if (options.SigningCredentials == null) 74 | { 75 | throw new ArgumentNullException(nameof(JwtIssuerOptions.SigningCredentials)); 76 | } 77 | 78 | if (options.JtiGenerator == null) 79 | { 80 | throw new ArgumentNullException(nameof(JwtIssuerOptions.JtiGenerator)); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Tests/Web.Api.Core.UnitTests/UseCases/LoginUseCaseUnitTests.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Moq; 3 | using Web.Api.Core.Domain.Entities; 4 | using Web.Api.Core.Dto; 5 | using Web.Api.Core.Dto.UseCaseRequests; 6 | using Web.Api.Core.Dto.UseCaseResponses; 7 | using Web.Api.Core.Interfaces; 8 | using Web.Api.Core.Interfaces.Gateways.Repositories; 9 | using Web.Api.Core.Interfaces.Services; 10 | using Web.Api.Core.UseCases; 11 | using Xunit; 12 | 13 | namespace Web.Api.Core.UnitTests.UseCases 14 | { 15 | public class LoginUseCaseUnitTests 16 | { 17 | [Fact] 18 | public async void Can_Login() 19 | { 20 | // arrange 21 | var mockUserRepository = new Mock(); 22 | mockUserRepository 23 | .Setup(repo => repo.FindByName(It.IsAny())) 24 | .Returns(Task.FromResult(new User("", "", "", ""))); 25 | 26 | mockUserRepository 27 | .Setup(repo => repo.CheckPassword(It.IsAny(), It.IsAny())) 28 | .Returns(Task.FromResult(true)); 29 | 30 | var mockJwtFactory = new Mock(); 31 | mockJwtFactory 32 | .Setup(repo => repo.GenerateEncodedToken(It.IsAny(), It.IsAny())) 33 | .Returns(Task.FromResult(new Token("", "", 0))); 34 | 35 | var useCase = new LoginUseCase(mockUserRepository.Object, mockJwtFactory.Object); 36 | 37 | var mockOutputPort = new Mock>(); 38 | mockOutputPort.Setup(outputPort => outputPort.Handle(It.IsAny())); 39 | 40 | // act 41 | var response = await useCase.Handle(new LoginRequest("userName", "password"), mockOutputPort.Object); 42 | 43 | // assert 44 | Assert.True(response); 45 | } 46 | 47 | [Fact] 48 | public async void Cant_Login_When_Missing_Username() 49 | { 50 | // arrange 51 | var mockUserRepository = new Mock(); 52 | mockUserRepository 53 | .Setup(repo => repo.FindByName(It.IsAny())) 54 | .Returns(Task.FromResult(new User("", "", "", ""))); 55 | 56 | mockUserRepository 57 | .Setup(repo => repo.CheckPassword(It.IsAny(), It.IsAny())) 58 | .Returns(Task.FromResult(true)); 59 | 60 | var mockJwtFactory = new Mock(); 61 | mockJwtFactory 62 | .Setup(repo => repo.GenerateEncodedToken(It.IsAny(), It.IsAny())) 63 | .Returns(Task.FromResult(new Token("", "", 0))); 64 | 65 | var useCase = new LoginUseCase(mockUserRepository.Object, mockJwtFactory.Object); 66 | 67 | var mockOutputPort = new Mock>(); 68 | mockOutputPort.Setup(outputPort => outputPort.Handle(It.IsAny())); 69 | 70 | // act 71 | var response = await useCase.Handle(new LoginRequest("", "password"), mockOutputPort.Object); 72 | 73 | // assert 74 | Assert.False(response); 75 | } 76 | 77 | 78 | [Fact] 79 | public async void Cant_Login_When_Unknown_Account() 80 | { 81 | // arrange 82 | var mockUserRepository = new Mock(); 83 | mockUserRepository 84 | .Setup(repo => repo.FindByName(It.IsAny())) 85 | .Returns(Task.FromResult(null)); 86 | 87 | mockUserRepository 88 | .Setup(repo => repo.CheckPassword(It.IsAny(), It.IsAny())) 89 | .Returns(Task.FromResult(true)); 90 | 91 | var mockJwtFactory = new Mock(); 92 | mockJwtFactory 93 | .Setup(repo => repo.GenerateEncodedToken(It.IsAny(), It.IsAny())) 94 | .Returns(Task.FromResult(new Token("", "", 0))); 95 | 96 | var useCase = new LoginUseCase(mockUserRepository.Object, mockJwtFactory.Object); 97 | 98 | var mockOutputPort = new Mock>(); 99 | mockOutputPort.Setup(outputPort => outputPort.Handle(It.IsAny())); 100 | 101 | // act 102 | var response = await useCase.Handle(new LoginRequest("", "password"), mockOutputPort.Object); 103 | 104 | // assert 105 | Assert.False(response); 106 | } 107 | 108 | [Fact] 109 | public async void Cant_Login_When_Password_Validation_Fails() 110 | { 111 | // arrange 112 | var mockUserRepository = new Mock(); 113 | mockUserRepository 114 | .Setup(repo => repo.FindByName(It.IsAny())) 115 | .Returns(Task.FromResult(null)); 116 | 117 | mockUserRepository 118 | .Setup(repo => repo.CheckPassword(It.IsAny(), It.IsAny())) 119 | .Returns(Task.FromResult(false)); 120 | 121 | var mockJwtFactory = new Mock(); 122 | mockJwtFactory 123 | .Setup(repo => repo.GenerateEncodedToken(It.IsAny(), It.IsAny())) 124 | .Returns(Task.FromResult(new Token("", "", 0))); 125 | 126 | var useCase = new LoginUseCase(mockUserRepository.Object, mockJwtFactory.Object); 127 | 128 | var mockOutputPort = new Mock>(); 129 | mockOutputPort.Setup(outputPort => outputPort.Handle(It.IsAny())); 130 | 131 | // act 132 | var response = await useCase.Handle(new LoginRequest("", "password"), mockOutputPort.Object); 133 | 134 | // assert 135 | Assert.False(response); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Web.Api/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Reflection; 4 | using System.Text; 5 | using Autofac; 6 | using Autofac.Extensions.DependencyInjection; 7 | using AutoMapper; 8 | using FluentValidation.AspNetCore; 9 | using Microsoft.AspNetCore.Authentication.JwtBearer; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Diagnostics; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Identity; 15 | using Microsoft.AspNetCore.Mvc; 16 | using Microsoft.EntityFrameworkCore; 17 | using Microsoft.Extensions.Configuration; 18 | using Microsoft.Extensions.DependencyInjection; 19 | using Microsoft.IdentityModel.Tokens; 20 | using Swashbuckle.AspNetCore.Swagger; 21 | using Web.Api.Core; 22 | using Web.Api.Extensions; 23 | using Web.Api.Infrastructure; 24 | using Web.Api.Infrastructure.Auth; 25 | using Web.Api.Infrastructure.Data.Entities; 26 | using Web.Api.Infrastructure.Data.EntityFramework; 27 | using Web.Api.Presenters; 28 | 29 | namespace Web.Api 30 | { 31 | public class Startup 32 | { 33 | private const string SecretKey = "iNivDmHLpUA223sqsfhqGbMRdRj1PVkH"; // todo: get this from somewhere secure 34 | private readonly SymmetricSecurityKey _signingKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(SecretKey)); 35 | 36 | public Startup(IConfiguration configuration) 37 | { 38 | Configuration = configuration; 39 | } 40 | 41 | public IConfiguration Configuration { get; } 42 | 43 | // This method gets called by the runtime. Use this method to add services to the container. 44 | public IServiceProvider ConfigureServices(IServiceCollection services) 45 | { 46 | // Add framework services. 47 | services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("Default"), b => b.MigrationsAssembly("Web.Api.Infrastructure"))); 48 | 49 | // jwt wire up 50 | // Get options from app settings 51 | var jwtAppSettingOptions = Configuration.GetSection(nameof(JwtIssuerOptions)); 52 | 53 | // Configure JwtIssuerOptions 54 | services.Configure(options => 55 | { 56 | options.Issuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; 57 | options.Audience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)]; 58 | options.SigningCredentials = new SigningCredentials(_signingKey, SecurityAlgorithms.HmacSha256); 59 | }); 60 | 61 | var tokenValidationParameters = new TokenValidationParameters 62 | { 63 | ValidateIssuer = true, 64 | ValidIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)], 65 | 66 | ValidateAudience = true, 67 | ValidAudience = jwtAppSettingOptions[nameof(JwtIssuerOptions.Audience)], 68 | 69 | ValidateIssuerSigningKey = true, 70 | IssuerSigningKey = _signingKey, 71 | 72 | RequireExpirationTime = false, 73 | ValidateLifetime = true, 74 | ClockSkew = TimeSpan.Zero 75 | }; 76 | 77 | services.AddAuthentication(options => 78 | { 79 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 80 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 81 | 82 | }).AddJwtBearer(configureOptions => 83 | { 84 | configureOptions.ClaimsIssuer = jwtAppSettingOptions[nameof(JwtIssuerOptions.Issuer)]; 85 | configureOptions.TokenValidationParameters = tokenValidationParameters; 86 | configureOptions.SaveToken = true; 87 | }); 88 | 89 | // add identity 90 | var identityBuilder = services.AddIdentityCore(o => 91 | { 92 | // configure identity options 93 | o.Password.RequireDigit = false; 94 | o.Password.RequireLowercase = false; 95 | o.Password.RequireUppercase = false; 96 | o.Password.RequireNonAlphanumeric = false; 97 | o.Password.RequiredLength = 6; 98 | }); 99 | 100 | identityBuilder = new IdentityBuilder(identityBuilder.UserType, typeof(IdentityRole), identityBuilder.Services); 101 | identityBuilder.AddEntityFrameworkStores().AddDefaultTokenProviders(); 102 | 103 | services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1).AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining()); 104 | 105 | services.AddAutoMapper(); 106 | 107 | // Register the Swagger generator, defining 1 or more Swagger documents 108 | services.AddSwaggerGen(c => 109 | { 110 | c.SwaggerDoc("v1", new Info { Title = "CleanAspNetCoreWebAPI", Version = "v1" }); 111 | }); 112 | 113 | // Now register our services with Autofac container. 114 | var builder = new ContainerBuilder(); 115 | 116 | builder.RegisterModule(new CoreModule()); 117 | builder.RegisterModule(new InfrastructureModule()); 118 | 119 | // Presenters 120 | builder.RegisterType().SingleInstance(); 121 | builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()).Where(t => t.Name.EndsWith("Presenter")).SingleInstance(); 122 | 123 | builder.Populate(services); 124 | var container = builder.Build(); 125 | // Create the IServiceProvider based on the container. 126 | return new AutofacServiceProvider(container); 127 | } 128 | 129 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 130 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 131 | { 132 | if (env.IsDevelopment()) 133 | { 134 | app.UseDeveloperExceptionPage(); 135 | } 136 | 137 | app.UseExceptionHandler( 138 | builder => 139 | { 140 | builder.Run( 141 | async context => 142 | { 143 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 144 | context.Response.Headers.Add("Access-Control-Allow-Origin", "*"); 145 | 146 | var error = context.Features.Get(); 147 | if (error != null) 148 | { 149 | context.Response.AddApplicationError(error.Error.Message); 150 | await context.Response.WriteAsync(error.Error.Message).ConfigureAwait(false); 151 | } 152 | }); 153 | }); 154 | 155 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), 156 | // specifying the Swagger JSON endpoint. 157 | app.UseSwaggerUI(c => 158 | { 159 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "CleanAspNetCoreWebAPI V1"); 160 | }); 161 | 162 | // Enable middleware to serve generated Swagger as a JSON endpoint. 163 | app.UseSwagger(); 164 | 165 | app.UseMvc(); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | using Web.Api.Infrastructure.Data.EntityFramework; 8 | 9 | namespace Web.Api.Infrastructure.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.2.0-preview2-35157") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd(); 26 | 27 | b.Property("ConcurrencyStamp") 28 | .IsConcurrencyToken(); 29 | 30 | b.Property("Name") 31 | .HasMaxLength(256); 32 | 33 | b.Property("NormalizedName") 34 | .HasMaxLength(256); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.HasIndex("NormalizedName") 39 | .IsUnique() 40 | .HasName("RoleNameIndex") 41 | .HasFilter("[NormalizedName] IS NOT NULL"); 42 | 43 | b.ToTable("AspNetRoles"); 44 | }); 45 | 46 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 47 | { 48 | b.Property("Id") 49 | .ValueGeneratedOnAdd() 50 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 51 | 52 | b.Property("ClaimType"); 53 | 54 | b.Property("ClaimValue"); 55 | 56 | b.Property("RoleId") 57 | .IsRequired(); 58 | 59 | b.HasKey("Id"); 60 | 61 | b.HasIndex("RoleId"); 62 | 63 | b.ToTable("AspNetRoleClaims"); 64 | }); 65 | 66 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 67 | { 68 | b.Property("Id") 69 | .ValueGeneratedOnAdd() 70 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 71 | 72 | b.Property("ClaimType"); 73 | 74 | b.Property("ClaimValue"); 75 | 76 | b.Property("UserId") 77 | .IsRequired(); 78 | 79 | b.HasKey("Id"); 80 | 81 | b.HasIndex("UserId"); 82 | 83 | b.ToTable("AspNetUserClaims"); 84 | }); 85 | 86 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 87 | { 88 | b.Property("LoginProvider"); 89 | 90 | b.Property("ProviderKey"); 91 | 92 | b.Property("ProviderDisplayName"); 93 | 94 | b.Property("UserId") 95 | .IsRequired(); 96 | 97 | b.HasKey("LoginProvider", "ProviderKey"); 98 | 99 | b.HasIndex("UserId"); 100 | 101 | b.ToTable("AspNetUserLogins"); 102 | }); 103 | 104 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 105 | { 106 | b.Property("UserId"); 107 | 108 | b.Property("RoleId"); 109 | 110 | b.HasKey("UserId", "RoleId"); 111 | 112 | b.HasIndex("RoleId"); 113 | 114 | b.ToTable("AspNetUserRoles"); 115 | }); 116 | 117 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 118 | { 119 | b.Property("UserId"); 120 | 121 | b.Property("LoginProvider"); 122 | 123 | b.Property("Name"); 124 | 125 | b.Property("Value"); 126 | 127 | b.HasKey("UserId", "LoginProvider", "Name"); 128 | 129 | b.ToTable("AspNetUserTokens"); 130 | }); 131 | 132 | modelBuilder.Entity("Web.Api.Infrastructure.Data.Entities.AppUser", b => 133 | { 134 | b.Property("Id") 135 | .ValueGeneratedOnAdd(); 136 | 137 | b.Property("AccessFailedCount"); 138 | 139 | b.Property("ConcurrencyStamp") 140 | .IsConcurrencyToken(); 141 | 142 | b.Property("Email") 143 | .HasMaxLength(256); 144 | 145 | b.Property("EmailConfirmed"); 146 | 147 | b.Property("FirstName"); 148 | 149 | b.Property("LastName"); 150 | 151 | b.Property("LockoutEnabled"); 152 | 153 | b.Property("LockoutEnd"); 154 | 155 | b.Property("NormalizedEmail") 156 | .HasMaxLength(256); 157 | 158 | b.Property("NormalizedUserName") 159 | .HasMaxLength(256); 160 | 161 | b.Property("PasswordHash"); 162 | 163 | b.Property("PhoneNumber"); 164 | 165 | b.Property("PhoneNumberConfirmed"); 166 | 167 | b.Property("SecurityStamp"); 168 | 169 | b.Property("TwoFactorEnabled"); 170 | 171 | b.Property("UserName") 172 | .HasMaxLength(256); 173 | 174 | b.HasKey("Id"); 175 | 176 | b.HasIndex("NormalizedEmail") 177 | .HasName("EmailIndex"); 178 | 179 | b.HasIndex("NormalizedUserName") 180 | .IsUnique() 181 | .HasName("UserNameIndex") 182 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 183 | 184 | b.ToTable("AspNetUsers"); 185 | }); 186 | 187 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 188 | { 189 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 190 | .WithMany() 191 | .HasForeignKey("RoleId") 192 | .OnDelete(DeleteBehavior.Cascade); 193 | }); 194 | 195 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 196 | { 197 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 198 | .WithMany() 199 | .HasForeignKey("UserId") 200 | .OnDelete(DeleteBehavior.Cascade); 201 | }); 202 | 203 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 204 | { 205 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 206 | .WithMany() 207 | .HasForeignKey("UserId") 208 | .OnDelete(DeleteBehavior.Cascade); 209 | }); 210 | 211 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 212 | { 213 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 214 | .WithMany() 215 | .HasForeignKey("RoleId") 216 | .OnDelete(DeleteBehavior.Cascade); 217 | 218 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 219 | .WithMany() 220 | .HasForeignKey("UserId") 221 | .OnDelete(DeleteBehavior.Cascade); 222 | }); 223 | 224 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 225 | { 226 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 227 | .WithMany() 228 | .HasForeignKey("UserId") 229 | .OnDelete(DeleteBehavior.Cascade); 230 | }); 231 | #pragma warning restore 612, 618 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Migrations/20181003180228_initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Metadata; 6 | using Microsoft.EntityFrameworkCore.Migrations; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | using Web.Api.Infrastructure.Data.EntityFramework; 9 | 10 | namespace Web.Api.Infrastructure.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20181003180228_initial")] 14 | partial class initial 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.2.0-preview2-35157") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("ConcurrencyStamp") 30 | .IsConcurrencyToken(); 31 | 32 | b.Property("Name") 33 | .HasMaxLength(256); 34 | 35 | b.Property("NormalizedName") 36 | .HasMaxLength(256); 37 | 38 | b.HasKey("Id"); 39 | 40 | b.HasIndex("NormalizedName") 41 | .IsUnique() 42 | .HasName("RoleNameIndex") 43 | .HasFilter("[NormalizedName] IS NOT NULL"); 44 | 45 | b.ToTable("AspNetRoles"); 46 | }); 47 | 48 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 49 | { 50 | b.Property("Id") 51 | .ValueGeneratedOnAdd() 52 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 53 | 54 | b.Property("ClaimType"); 55 | 56 | b.Property("ClaimValue"); 57 | 58 | b.Property("RoleId") 59 | .IsRequired(); 60 | 61 | b.HasKey("Id"); 62 | 63 | b.HasIndex("RoleId"); 64 | 65 | b.ToTable("AspNetRoleClaims"); 66 | }); 67 | 68 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 69 | { 70 | b.Property("Id") 71 | .ValueGeneratedOnAdd() 72 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 73 | 74 | b.Property("ClaimType"); 75 | 76 | b.Property("ClaimValue"); 77 | 78 | b.Property("UserId") 79 | .IsRequired(); 80 | 81 | b.HasKey("Id"); 82 | 83 | b.HasIndex("UserId"); 84 | 85 | b.ToTable("AspNetUserClaims"); 86 | }); 87 | 88 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 89 | { 90 | b.Property("LoginProvider"); 91 | 92 | b.Property("ProviderKey"); 93 | 94 | b.Property("ProviderDisplayName"); 95 | 96 | b.Property("UserId") 97 | .IsRequired(); 98 | 99 | b.HasKey("LoginProvider", "ProviderKey"); 100 | 101 | b.HasIndex("UserId"); 102 | 103 | b.ToTable("AspNetUserLogins"); 104 | }); 105 | 106 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 107 | { 108 | b.Property("UserId"); 109 | 110 | b.Property("RoleId"); 111 | 112 | b.HasKey("UserId", "RoleId"); 113 | 114 | b.HasIndex("RoleId"); 115 | 116 | b.ToTable("AspNetUserRoles"); 117 | }); 118 | 119 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 120 | { 121 | b.Property("UserId"); 122 | 123 | b.Property("LoginProvider"); 124 | 125 | b.Property("Name"); 126 | 127 | b.Property("Value"); 128 | 129 | b.HasKey("UserId", "LoginProvider", "Name"); 130 | 131 | b.ToTable("AspNetUserTokens"); 132 | }); 133 | 134 | modelBuilder.Entity("Web.Api.Infrastructure.Data.Entities.AppUser", b => 135 | { 136 | b.Property("Id") 137 | .ValueGeneratedOnAdd(); 138 | 139 | b.Property("AccessFailedCount"); 140 | 141 | b.Property("ConcurrencyStamp") 142 | .IsConcurrencyToken(); 143 | 144 | b.Property("Email") 145 | .HasMaxLength(256); 146 | 147 | b.Property("EmailConfirmed"); 148 | 149 | b.Property("FirstName"); 150 | 151 | b.Property("LastName"); 152 | 153 | b.Property("LockoutEnabled"); 154 | 155 | b.Property("LockoutEnd"); 156 | 157 | b.Property("NormalizedEmail") 158 | .HasMaxLength(256); 159 | 160 | b.Property("NormalizedUserName") 161 | .HasMaxLength(256); 162 | 163 | b.Property("PasswordHash"); 164 | 165 | b.Property("PhoneNumber"); 166 | 167 | b.Property("PhoneNumberConfirmed"); 168 | 169 | b.Property("SecurityStamp"); 170 | 171 | b.Property("TwoFactorEnabled"); 172 | 173 | b.Property("UserName") 174 | .HasMaxLength(256); 175 | 176 | b.HasKey("Id"); 177 | 178 | b.HasIndex("NormalizedEmail") 179 | .HasName("EmailIndex"); 180 | 181 | b.HasIndex("NormalizedUserName") 182 | .IsUnique() 183 | .HasName("UserNameIndex") 184 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 185 | 186 | b.ToTable("AspNetUsers"); 187 | }); 188 | 189 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 190 | { 191 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 192 | .WithMany() 193 | .HasForeignKey("RoleId") 194 | .OnDelete(DeleteBehavior.Cascade); 195 | }); 196 | 197 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 198 | { 199 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 200 | .WithMany() 201 | .HasForeignKey("UserId") 202 | .OnDelete(DeleteBehavior.Cascade); 203 | }); 204 | 205 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 206 | { 207 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 208 | .WithMany() 209 | .HasForeignKey("UserId") 210 | .OnDelete(DeleteBehavior.Cascade); 211 | }); 212 | 213 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 214 | { 215 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") 216 | .WithMany() 217 | .HasForeignKey("RoleId") 218 | .OnDelete(DeleteBehavior.Cascade); 219 | 220 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 221 | .WithMany() 222 | .HasForeignKey("UserId") 223 | .OnDelete(DeleteBehavior.Cascade); 224 | }); 225 | 226 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 227 | { 228 | b.HasOne("Web.Api.Infrastructure.Data.Entities.AppUser") 229 | .WithMany() 230 | .HasForeignKey("UserId") 231 | .OnDelete(DeleteBehavior.Cascade); 232 | }); 233 | #pragma warning restore 612, 618 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Web.Api.Infrastructure/Migrations/20181003180228_initial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Metadata; 3 | using Microsoft.EntityFrameworkCore.Migrations; 4 | 5 | namespace Web.Api.Infrastructure.Migrations 6 | { 7 | public partial class initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "AspNetRoles", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false), 16 | Name = table.Column(maxLength: 256, nullable: true), 17 | NormalizedName = table.Column(maxLength: 256, nullable: true), 18 | ConcurrencyStamp = table.Column(nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_AspNetRoles", x => x.Id); 23 | }); 24 | 25 | migrationBuilder.CreateTable( 26 | name: "AspNetUsers", 27 | columns: table => new 28 | { 29 | Id = table.Column(nullable: false), 30 | UserName = table.Column(maxLength: 256, nullable: true), 31 | NormalizedUserName = table.Column(maxLength: 256, nullable: true), 32 | Email = table.Column(maxLength: 256, nullable: true), 33 | NormalizedEmail = table.Column(maxLength: 256, nullable: true), 34 | EmailConfirmed = table.Column(nullable: false), 35 | PasswordHash = table.Column(nullable: true), 36 | SecurityStamp = table.Column(nullable: true), 37 | ConcurrencyStamp = table.Column(nullable: true), 38 | PhoneNumber = table.Column(nullable: true), 39 | PhoneNumberConfirmed = table.Column(nullable: false), 40 | TwoFactorEnabled = table.Column(nullable: false), 41 | LockoutEnd = table.Column(nullable: true), 42 | LockoutEnabled = table.Column(nullable: false), 43 | AccessFailedCount = table.Column(nullable: false), 44 | FirstName = table.Column(nullable: true), 45 | LastName = table.Column(nullable: true) 46 | }, 47 | constraints: table => 48 | { 49 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 50 | }); 51 | 52 | migrationBuilder.CreateTable( 53 | name: "AspNetRoleClaims", 54 | columns: table => new 55 | { 56 | Id = table.Column(nullable: false) 57 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 58 | RoleId = table.Column(nullable: false), 59 | ClaimType = table.Column(nullable: true), 60 | ClaimValue = table.Column(nullable: true) 61 | }, 62 | constraints: table => 63 | { 64 | table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); 65 | table.ForeignKey( 66 | name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", 67 | column: x => x.RoleId, 68 | principalTable: "AspNetRoles", 69 | principalColumn: "Id", 70 | onDelete: ReferentialAction.Cascade); 71 | }); 72 | 73 | migrationBuilder.CreateTable( 74 | name: "AspNetUserClaims", 75 | columns: table => new 76 | { 77 | Id = table.Column(nullable: false) 78 | .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), 79 | UserId = table.Column(nullable: false), 80 | ClaimType = table.Column(nullable: true), 81 | ClaimValue = table.Column(nullable: true) 82 | }, 83 | constraints: table => 84 | { 85 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 86 | table.ForeignKey( 87 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 88 | column: x => x.UserId, 89 | principalTable: "AspNetUsers", 90 | principalColumn: "Id", 91 | onDelete: ReferentialAction.Cascade); 92 | }); 93 | 94 | migrationBuilder.CreateTable( 95 | name: "AspNetUserLogins", 96 | columns: table => new 97 | { 98 | LoginProvider = table.Column(nullable: false), 99 | ProviderKey = table.Column(nullable: false), 100 | ProviderDisplayName = table.Column(nullable: true), 101 | UserId = table.Column(nullable: false) 102 | }, 103 | constraints: table => 104 | { 105 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 106 | table.ForeignKey( 107 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 108 | column: x => x.UserId, 109 | principalTable: "AspNetUsers", 110 | principalColumn: "Id", 111 | onDelete: ReferentialAction.Cascade); 112 | }); 113 | 114 | migrationBuilder.CreateTable( 115 | name: "AspNetUserRoles", 116 | columns: table => new 117 | { 118 | UserId = table.Column(nullable: false), 119 | RoleId = table.Column(nullable: false) 120 | }, 121 | constraints: table => 122 | { 123 | table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); 124 | table.ForeignKey( 125 | name: "FK_AspNetUserRoles_AspNetRoles_RoleId", 126 | column: x => x.RoleId, 127 | principalTable: "AspNetRoles", 128 | principalColumn: "Id", 129 | onDelete: ReferentialAction.Cascade); 130 | table.ForeignKey( 131 | name: "FK_AspNetUserRoles_AspNetUsers_UserId", 132 | column: x => x.UserId, 133 | principalTable: "AspNetUsers", 134 | principalColumn: "Id", 135 | onDelete: ReferentialAction.Cascade); 136 | }); 137 | 138 | migrationBuilder.CreateTable( 139 | name: "AspNetUserTokens", 140 | columns: table => new 141 | { 142 | UserId = table.Column(nullable: false), 143 | LoginProvider = table.Column(nullable: false), 144 | Name = table.Column(nullable: false), 145 | Value = table.Column(nullable: true) 146 | }, 147 | constraints: table => 148 | { 149 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 150 | table.ForeignKey( 151 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 152 | column: x => x.UserId, 153 | principalTable: "AspNetUsers", 154 | principalColumn: "Id", 155 | onDelete: ReferentialAction.Cascade); 156 | }); 157 | 158 | migrationBuilder.CreateIndex( 159 | name: "IX_AspNetRoleClaims_RoleId", 160 | table: "AspNetRoleClaims", 161 | column: "RoleId"); 162 | 163 | migrationBuilder.CreateIndex( 164 | name: "RoleNameIndex", 165 | table: "AspNetRoles", 166 | column: "NormalizedName", 167 | unique: true, 168 | filter: "[NormalizedName] IS NOT NULL"); 169 | 170 | migrationBuilder.CreateIndex( 171 | name: "IX_AspNetUserClaims_UserId", 172 | table: "AspNetUserClaims", 173 | column: "UserId"); 174 | 175 | migrationBuilder.CreateIndex( 176 | name: "IX_AspNetUserLogins_UserId", 177 | table: "AspNetUserLogins", 178 | column: "UserId"); 179 | 180 | migrationBuilder.CreateIndex( 181 | name: "IX_AspNetUserRoles_RoleId", 182 | table: "AspNetUserRoles", 183 | column: "RoleId"); 184 | 185 | migrationBuilder.CreateIndex( 186 | name: "EmailIndex", 187 | table: "AspNetUsers", 188 | column: "NormalizedEmail"); 189 | 190 | migrationBuilder.CreateIndex( 191 | name: "UserNameIndex", 192 | table: "AspNetUsers", 193 | column: "NormalizedUserName", 194 | unique: true, 195 | filter: "[NormalizedUserName] IS NOT NULL"); 196 | } 197 | 198 | protected override void Down(MigrationBuilder migrationBuilder) 199 | { 200 | migrationBuilder.DropTable( 201 | name: "AspNetRoleClaims"); 202 | 203 | migrationBuilder.DropTable( 204 | name: "AspNetUserClaims"); 205 | 206 | migrationBuilder.DropTable( 207 | name: "AspNetUserLogins"); 208 | 209 | migrationBuilder.DropTable( 210 | name: "AspNetUserRoles"); 211 | 212 | migrationBuilder.DropTable( 213 | name: "AspNetUserTokens"); 214 | 215 | migrationBuilder.DropTable( 216 | name: "AspNetRoles"); 217 | 218 | migrationBuilder.DropTable( 219 | name: "AspNetUsers"); 220 | } 221 | } 222 | } 223 | --------------------------------------------------------------------------------