├── DDD.NetCore.Web ├── wwwroot │ ├── js │ │ ├── site.min.js │ │ └── site.js │ ├── favicon.ico │ ├── lib │ │ ├── bootstrap │ │ │ ├── dist │ │ │ │ ├── fonts │ │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ │ └── js │ │ │ │ │ └── npm.js │ │ │ ├── .bower.json │ │ │ └── LICENSE │ │ ├── jquery │ │ │ ├── .bower.json │ │ │ └── LICENSE.txt │ │ ├── jquery-validation │ │ │ ├── .bower.json │ │ │ └── LICENSE.md │ │ └── jquery-validation-unobtrusive │ │ │ ├── .bower.json │ │ │ └── jquery.validate.unobtrusive.min.js │ ├── css │ │ ├── site.min.css │ │ └── site.css │ └── images │ │ └── banner2.svg ├── .bowerrc ├── Pages │ ├── _ViewStart.cshtml │ ├── Account │ │ ├── _ViewImports.cshtml │ │ ├── Manage │ │ │ ├── _ViewImports.cshtml │ │ │ ├── _StatusMessage.cshtml │ │ │ ├── _Layout.cshtml │ │ │ ├── _ManageNav.cshtml │ │ │ ├── Disable2fa.cshtml │ │ │ ├── GenerateRecoveryCodes.cshtml │ │ │ ├── ResetAuthenticator.cshtml │ │ │ ├── SetPassword.cshtml │ │ │ ├── ChangePassword.cshtml │ │ │ ├── ManageNavPages.cs │ │ │ ├── GenerateRecoveryCodes.cshtml.cs │ │ │ ├── TwoFactorAuthentication.cshtml │ │ │ ├── ResetAuthenticator.cshtml.cs │ │ │ ├── Index.cshtml │ │ │ ├── TwoFactorAuthentication.cshtml.cs │ │ │ ├── ExternalLogins.cshtml │ │ │ ├── Disable2fa.cshtml.cs │ │ │ ├── EnableAuthenticator.cshtml │ │ │ ├── SetPassword.cshtml.cs │ │ │ ├── ChangePassword.cshtml.cs │ │ │ ├── Index.cshtml.cs │ │ │ ├── ExternalLogins.cshtml.cs │ │ │ └── EnableAuthenticator.cshtml.cs │ │ ├── SignedOut.cshtml │ │ ├── ConfirmEmail.cshtml │ │ ├── ForgotPasswordConfirmation.cshtml │ │ ├── AccessDenied.cshtml │ │ ├── Lockout.cshtml │ │ ├── ResetPasswordConfirmation.cshtml │ │ ├── Lockout.cshtml.cs │ │ ├── AccessDenied.cshtml.cs │ │ ├── ForgotPasswordConfirmation.cshtml.cs │ │ ├── ResetPasswordConfirmation.cshtml.cs │ │ ├── SignedOut.cshtml.cs │ │ ├── ForgotPassword.cshtml │ │ ├── LoginWithRecoveryCode.cshtml │ │ ├── ExternalLogin.cshtml │ │ ├── ConfirmEmail.cshtml.cs │ │ ├── Register.cshtml │ │ ├── ResetPassword.cshtml │ │ ├── LoginWith2fa.cshtml │ │ ├── ForgotPassword.cshtml.cs │ │ ├── ResetPassword.cshtml.cs │ │ ├── LoginWithRecoveryCode.cshtml.cs │ │ ├── Login.cshtml │ │ ├── Register.cshtml.cs │ │ ├── LoginWith2fa.cshtml.cs │ │ ├── Login.cshtml.cs │ │ └── ExternalLogin.cshtml.cs │ ├── _ViewImports.cshtml │ ├── About.cshtml │ ├── Index.cshtml.cs │ ├── Contact.cshtml.cs │ ├── About.cshtml.cs │ ├── Contact.cshtml │ ├── Error.cshtml.cs │ ├── Error.cshtml │ ├── _LoginPartial.cshtml │ ├── _ValidationScriptsPartial.cshtml │ ├── _Layout.cshtml │ └── Index.cshtml ├── .dockerignore ├── Dockerfile ├── appsettings.Development.json ├── bower.json ├── appsettings.json ├── Services │ ├── IEmailSender.cs │ └── EmailSender.cs ├── Data │ ├── ApplicationUser.cs │ └── ApplicationDbContext.cs ├── Program.cs ├── bundleconfig.json ├── Properties │ └── launchSettings.json ├── Extensions │ ├── EmailSenderExtensions.cs │ └── UrlHelperExtensions.cs ├── Controllers │ └── AccountController.cs ├── DDD.NetCore.Web.csproj └── Startup.cs ├── DDD.NetCore ├── Application │ └── IApplicationService.cs ├── Domain │ ├── Entities │ │ ├── IAggregateRoot.cs │ │ ├── AggregateRoot.cs │ │ ├── IPassivable.cs │ │ ├── ISoftDelete.cs │ │ ├── IExtendableObject.cs │ │ ├── IEntity.cs │ │ └── Entity.cs │ ├── Uow │ │ ├── IUnitOfWork.cs │ │ ├── UnitOfWorkServiceCollectionExtensions.cs │ │ └── UnitOfWork.cs │ └── Repositories │ │ ├── EfCoreRepository.cs │ │ └── RepositoryBase.cs ├── Exception │ ├── ExceptionBase.cs │ └── EntityNotFoundException.cs └── DDD.NetCore.csproj ├── DDD.NetCore.Domain ├── Goods │ ├── IGoodsRepository.cs │ ├── GoodsCategory.cs │ └── Goods.cs ├── Authorization │ └── ApplicationUser.cs ├── Customers │ ├── ICustomerRepository.cs │ ├── Customer.cs │ └── Address.cs ├── ShoppingCarts │ ├── IShoppingCartRepository.cs │ ├── ShoppingCartLine.cs │ └── ShoppingCart.cs ├── DDD.NetCore.Domain.csproj └── Orders │ └── SaleOrder.cs ├── DDD.NetCore.Application ├── DDD.NetCore.Application.csproj ├── ShoppingCarts │ ├── IShoppingCartService.cs │ └── ShoppingCartService.cs └── Users │ └── IUserAppService.cs ├── DDD.NetCore.Infrastructure ├── DDD.NetCore.Infrastructure.csproj └── EfCore │ └── ApplicationDbContext.cs ├── DDD.NetCore.Test ├── TestData │ ├── UsersTestData.cs │ └── GoodsTestData.cs ├── DDD.NetCore.Test.csproj ├── TestBase.cs └── UnitTest1.cs ├── .gitattributes ├── DDD.NetCore.sln └── .gitignore /DDD.NetCore.Web/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "wwwroot/lib" 3 | } 4 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your Javascript code. 2 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !obj/Docker/publish/* 3 | !obj/Docker/empty/ 4 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using DDD.NetCore.Web.Pages.Account 2 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using DDD.NetCore.Web.Pages.Account.Manage 2 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheng-jie/DDD.NetCore/HEAD/DDD.NetCore.Web/wwwroot/favicon.ico -------------------------------------------------------------------------------- /DDD.NetCore/Application/IApplicationService.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Application 2 | { 3 | public interface IApplicationService 4 | { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Goods/IGoodsRepository.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace DDD.NetCore.Domain.Goods 4 | { 5 | public interface IGoodsRepository 6 | { 7 | Goods Find(int id); 8 | } 9 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:2.0 2 | ARG source 3 | WORKDIR /app 4 | EXPOSE 80 5 | COPY ${source:-obj/Docker/publish} . 6 | ENTRYPOINT ["dotnet", "DDD.NetCore.Web.dll"] 7 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheng-jie/DDD.NetCore/HEAD/DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheng-jie/DDD.NetCore/HEAD/DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheng-jie/DDD.NetCore/HEAD/DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sheng-jie/DDD.NetCore/HEAD/DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Authorization/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace DDD.NetCore.Domain.Authorization 4 | { 5 | public class ApplicationUser : IdentityUser 6 | { 7 | 8 | } 9 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using DDD.NetCore.Web 3 | @using DDD.NetCore.Web.Data 4 | @namespace DDD.NetCore.Web.Pages 5 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 6 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/SignedOut.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model SignedOutModel 3 | @{ 4 | ViewData["Title"] = "Signed out"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | You have successfully signed out. 10 |

11 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/About.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AboutModel 3 | @{ 4 | ViewData["Title"] = "About"; 5 | } 6 |

@ViewData["Title"]

7 |

@Model.Message

8 | 9 |

Use this area to provide additional information.

10 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asp.net", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap": "3.3.7", 6 | "jquery": "2.2.0", 7 | "jquery-validation": "1.14.0", 8 | "jquery-validation-unobtrusive": "3.2.6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ConfirmEmail.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ConfirmEmailModel 3 | @{ 4 | ViewData["Title"] = "Confirm email"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

10 | Thank you for confirming your email. 11 |

12 |
13 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ForgotPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ForgotPasswordConfirmation 3 | @{ 4 | ViewData["Title"] = "Forgot password confirmation"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | Please check your email to reset your password. 10 |

11 | -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Entities 2 | { 3 | public interface IAggregateRoot : IAggregateRoot, IEntity 4 | { 5 | 6 | } 7 | 8 | public interface IAggregateRoot : IEntity 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/AccessDenied.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model AccessDeniedModel 3 | @{ 4 | ViewData["Title"] = "Access denied"; 5 | } 6 | 7 |
8 |

ViewData["Title"]

9 |

You do not have access to this resource.

10 |
11 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Lockout.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LockoutModel 3 | @{ 4 | ViewData["Title"] = "Locked out"; 5 | } 6 | 7 |
8 |

@ViewData["Title"]

9 |

This account has been locked out, please try again later.

10 |
11 | -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Customers/ICustomerRepository.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Customers 2 | { 3 | public interface ICustomerRepository 4 | { 5 | void Add(Customer customer); 6 | 7 | Customer GetCustomerByUserId(string userId); 8 | 9 | void Update(Customer customer); 10 | } 11 | } -------------------------------------------------------------------------------- /DDD.NetCore.Domain/ShoppingCarts/IShoppingCartRepository.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.ShoppingCarts 2 | { 3 | public interface IShoppingCartRepository 4 | { 5 | ShoppingCart Find(int cartId); 6 | 7 | void Add(ShoppingCart cart); 8 | 9 | void Update(ShoppingCart cart); 10 | } 11 | } -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Entities 2 | { 3 | public class AggregateRoot : AggregateRoot, IAggregateRoot 4 | { 5 | 6 | } 7 | 8 | public class AggregateRoot : Entity, IAggregateRoot 9 | { 10 | 11 | } 12 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ResetPasswordConfirmation.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetPasswordConfirmationModel 3 | @{ 4 | ViewData["Title"] = "Reset password confirmation"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

9 | Your password has been reset. Please click here to log in. 10 |

11 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=DDD.NetCore.Web;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "IncludeScopes": false, 7 | "LogLevel": { 8 | "Default": "Warning" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /DDD.NetCore.Application/DDD.NetCore.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Services/IEmailSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DDD.NetCore.Web.Services 7 | { 8 | public interface IEmailSender 9 | { 10 | Task SendEmailAsync(string email, string subject, string message); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Lockout.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace DDD.NetCore.Web.Pages.Account 8 | { 9 | public class LockoutModel : PageModel 10 | { 11 | public void OnGet() 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/AccessDenied.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace DDD.NetCore.Web.Pages.Account 8 | { 9 | public class AccessDeniedModel : PageModel 10 | { 11 | public void OnGet() 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Data/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | namespace DDD.NetCore.Web.Data 8 | { 9 | // Add profile data for application users by adding properties to the ApplicationUser class 10 | public class ApplicationUser : IdentityUser 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace DDD.NetCore.Web.Pages 9 | { 10 | public class IndexModel : PageModel 11 | { 12 | public void OnGet() 13 | { 14 | 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ForgotPasswordConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace DDD.NetCore.Web.Pages.Account 8 | { 9 | public class ForgotPasswordConfirmation : PageModel 10 | { 11 | public void OnGet() 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ResetPasswordConfirmation.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace DDD.NetCore.Web.Pages.Account 8 | { 9 | public class ResetPasswordConfirmationModel : PageModel 10 | { 11 | public void OnGet() 12 | { 13 | 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DDD.NetCore/Exception/ExceptionBase.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Exception 2 | { 3 | public class ExceptionBase : System.Exception 4 | { 5 | public ExceptionBase() 6 | { 7 | } 8 | 9 | public ExceptionBase(string message) : base(message) 10 | { 11 | } 12 | 13 | public ExceptionBase(string message, System.Exception inner) : base(message, inner) 14 | { 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/IPassivable.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Entities 2 | { 3 | /// 4 | /// This interface is used to make an entity active/passive. 5 | /// 6 | public interface IPassivable 7 | { 8 | /// 9 | /// True: This entity is active. 10 | /// False: This entity is not active. 11 | /// 12 | bool IsActive { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Goods/GoodsCategory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DDD.NetCore.Domain.Entities; 3 | 4 | namespace DDD.NetCore.Domain.Goods 5 | { 6 | /// 7 | /// 商品类别 8 | /// 9 | public class GoodsCategory : Entity 10 | { 11 | public string Name { get; set; } 12 | public string Description { get; set; } 13 | 14 | public List GoodsList { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/_StatusMessage.cshtml: -------------------------------------------------------------------------------- 1 | @model string 2 | 3 | @if (!String.IsNullOrEmpty(Model)) 4 | { 5 | var statusMessageClass = Model.StartsWith("Error") ? "danger" : "success"; 6 | 10 | } 11 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Contact.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace DDD.NetCore.Web.Pages 8 | { 9 | public class ContactModel : PageModel 10 | { 11 | public string Message { get; set; } 12 | 13 | public void OnGet() 14 | { 15 | Message = "Your contact page."; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/About.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.RazorPages; 6 | 7 | namespace DDD.NetCore.Web.Pages 8 | { 9 | public class AboutModel : PageModel 10 | { 11 | public string Message { get; set; } 12 | 13 | public void OnGet() 14 | { 15 | Message = "Your application description page."; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /DDD.NetCore.Infrastructure/DDD.NetCore.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /DDD.NetCore.Application/ShoppingCarts/IShoppingCartService.cs: -------------------------------------------------------------------------------- 1 | using DDD.NetCore.Domain.ShoppingCarts; 2 | 3 | namespace DDD.NetCore.Application.ShoppingCarts 4 | { 5 | public interface IShoppingCartService 6 | { 7 | void AddGoodsToCart(string userId, int goodsId, int qty); 8 | void ChangeItmeQty(int cartId, int cartLineId, int qty); 9 | void ClearCart(int cartId); 10 | void RemoveItemFromCart(int cartId, int cartLineId); 11 | 12 | ShoppingCart GetShoppingCart(int cartId); 13 | } 14 | } -------------------------------------------------------------------------------- /DDD.NetCore.Domain/DDD.NetCore.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /DDD.NetCore.Domain/ShoppingCarts/ShoppingCartLine.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using DDD.NetCore.Domain.Entities; 3 | 4 | namespace DDD.NetCore.Domain.ShoppingCarts 5 | { 6 | public class ShoppingCartLine : Entity 7 | { 8 | public int ShoppingCartId { get; set; } 9 | public virtual ShoppingCart ShoppingCart { get; set; } 10 | 11 | public int GoodsId { get; set; } 12 | 13 | public virtual Goods.Goods Goods { get; set; } 14 | 15 | public int Qty { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "/Pages/_Layout.cshtml"; 3 | } 4 | 5 |

Manage your account

6 | 7 |
8 |

Change your account settings

9 |
10 |
11 |
12 | @await Html.PartialAsync("_ManageNav") 13 |
14 |
15 | @RenderBody() 16 |
17 |
18 |
19 | 20 | @section Scripts { 21 | @RenderSection("Scripts", required: false) 22 | } 23 | 24 | -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/ISoftDelete.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Entities 2 | { 3 | /// 4 | /// Used to standardize soft deleting entities. 5 | /// Soft-delete entities are not actually deleted, 6 | /// marked as IsDeleted = true in the database, 7 | /// but can not be retrieved to the application. 8 | /// 9 | public interface ISoftDelete 10 | { 11 | /// 12 | /// Used to mark an Entity as 'Deleted'. 13 | /// 14 | bool IsDeleted { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Contact.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ContactModel 3 | @{ 4 | ViewData["Title"] = "Contact"; 5 | } 6 |

@ViewData["Title"]

7 |

@Model.Message

8 | 9 |
10 | One Microsoft Way
11 | Redmond, WA 98052-6399
12 | P: 13 | 425.555.0100 14 |
15 | 16 |
17 | Support: Support@example.com
18 | Marketing: Marketing@example.com 19 |
20 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/SignedOut.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | public class SignedOutModel : PageModel 9 | { 10 | public IActionResult OnGet() 11 | { 12 | if (User.Identity.IsAuthenticated) 13 | { 14 | // Redirect to home page if the user is authenticated. 15 | return RedirectToPage("/Index"); 16 | } 17 | 18 | return Page(); 19 | } 20 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Services/EmailSender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace DDD.NetCore.Web.Services 7 | { 8 | // This class is used by the application to send email for account confirmation and password reset. 9 | // For more details see https://go.microsoft.com/fwlink/?LinkID=532713 10 | public class EmailSender : IEmailSender 11 | { 12 | public Task SendEmailAsync(string email, string subject, string message) 13 | { 14 | return Task.CompletedTask; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc.RazorPages; 7 | 8 | namespace DDD.NetCore.Web.Pages 9 | { 10 | public class ErrorModel : PageModel 11 | { 12 | public string RequestId { get; set; } 13 | 14 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 15 | 16 | public void OnGet() 17 | { 18 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "2.2.0", 16 | "_release": "2.2.0", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "2.2.0", 20 | "commit": "6fc01e29bdad0964f62ef56d01297039cdcadbe5" 21 | }, 22 | "_source": "git://github.com/jquery/jquery-dist.git", 23 | "_target": "2.2.0", 24 | "_originalSource": "jquery" 25 | } -------------------------------------------------------------------------------- /DDD.NetCore/DDD.NetCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Goods/Goods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DDD.NetCore.Domain.Entities; 3 | 4 | namespace DDD.NetCore.Domain.Goods 5 | { 6 | public class Goods : AggregateRoot 7 | { 8 | public string Name { get; set; } 9 | 10 | public string Description { get; set; } 11 | 12 | public decimal Price { get; set; } 13 | 14 | public int Stock { get; set; } 15 | 16 | public int GoodsCategoryId { get; set; } 17 | 18 | public virtual GoodsCategory GoodsCategory { get; set; } 19 | 20 | public DateTime CreateTime { get; private set; } 21 | 22 | public Goods() 23 | { 24 | CreateTime = DateTime.Now; 25 | } 26 | 27 | 28 | } 29 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/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 DDD.NetCore.Web 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | BuildWebHost(args).Run(); 18 | } 19 | 20 | public static IWebHost BuildWebHost(string[] args) => 21 | WebHost.CreateDefaultBuilder(args) 22 | .UseStartup() 23 | .Build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/IExtendableObject.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Entities 2 | { 3 | public interface IExtendableObject 4 | { 5 | /// 6 | /// A JSON formatted string to extend the containing object. 7 | /// JSON data can contain properties with arbitrary values (like primitives or complex objects). 8 | /// Extension methods are available () to manipulate this data. 9 | /// General format: 10 | /// 11 | /// { 12 | /// "Property1" : ... 13 | /// "Property2" : ... 14 | /// } 15 | /// 16 | /// 17 | string ExtensionData { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Customers/Customer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using DDD.NetCore.Domain.Authorization; 3 | using DDD.NetCore.Domain.Entities; 4 | using DDD.NetCore.Domain.ShoppingCarts; 5 | 6 | namespace DDD.NetCore.Domain.Customers 7 | { 8 | /// 9 | /// 客户 10 | /// 11 | public class Customer : AggregateRoot 12 | { 13 | public string NickName { get; set; } 14 | 15 | public string ApplicationUserId { get; set; } 16 | public virtual ApplicationUser ApplicationUser { get; set; } 17 | 18 | public int ShoppingCartId { get; set; } 19 | 20 | public virtual ShoppingCart ShoppingCart { get; set; } 21 | 22 | public virtual List
ShippingAddresses { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Carousel */ 14 | .carousel-caption p { 15 | font-size: 20px; 16 | line-height: 1.4; 17 | } 18 | 19 | /* Make .svg files in the carousel display properly in older browsers */ 20 | .carousel-inner .item img[src$=".svg"] { 21 | width: 100%; 22 | } 23 | 24 | /* QR code generator */ 25 | #qrCode { 26 | margin: 15px; 27 | } 28 | 29 | /* Hide/rearrange for smaller screens */ 30 | @media screen and (max-width: 767px) { 31 | /* Hide captions */ 32 | .carousel-caption { 33 | display: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/_ManageNav.cshtml: -------------------------------------------------------------------------------- 1 | @inject SignInManager SignInManager 2 | @{ 3 | var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); 4 | } 5 | 6 | 15 | 16 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:2208/", 7 | "sslPort": 44399 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "launchUrl": "https://localhost:44399/", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | } 18 | }, 19 | "DDD.NetCore.Web": { 20 | "commandName": "Project", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "http://localhost:2209/" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /DDD.NetCore.Test/TestData/UsersTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using DDD.NetCore.Domain.Authorization; 3 | using DDD.NetCore.Infrastructure.EfCore; 4 | 5 | namespace DDD.NetCore.Test.TestData 6 | { 7 | public static class UsersTestData 8 | { 9 | public static void CreateTestUser(ApplicationDbContext context) 10 | { 11 | var user = context.Users.FirstOrDefault(u => u.UserName == "Admin"); 12 | if (user==null) 13 | { 14 | user =new ApplicationUser() 15 | { 16 | UserName = "Adminn", 17 | Email = "admin@netcore.com", 18 | PasswordHash = "123qwe" 19 | }; 20 | context.Users.Add(user); 21 | context.SaveChanges(); 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Uow/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DDD.NetCore.Domain.Entities; 3 | using DDD.NetCore.Domain.Repositories; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Storage; 6 | 7 | namespace DDD.NetCore.Domain.Uow 8 | { 9 | public interface IUnitOfWork : IDisposable 10 | { 11 | IRepository GetRepository() where TEntity : class, IEntity; 12 | IRepository GetRepository() 13 | where TEntity : class, IEntity; 14 | 15 | 16 | void SaveChanges(); 17 | 18 | IDbContextTransaction BeginTransaction(); 19 | 20 | } 21 | 22 | public interface IUnitOfWork : IUnitOfWork where TDbContext : DbContext 23 | { 24 | TDbContext DbContext { get; } 25 | } 26 | } -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace DDD.NetCore.Domain.Entities 2 | { 3 | public interface IEntity : IEntity 4 | { 5 | } 6 | 7 | /// 8 | /// Defines interface for base entity type. All entities in the system must implement this interface. 9 | /// 10 | /// Type of the primary key of the entity 11 | public interface IEntity 12 | { 13 | /// 14 | /// Unique identifier for this entity. 15 | /// 16 | TPrimaryKey Id { get; set; } 17 | 18 | /// 19 | /// Checks if this entity is transient (not persisted to database and it has not an ). 20 | /// 21 | /// True, if this entity is transient 22 | bool IsTransient(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ForgotPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ForgotPasswordModel 3 | @{ 4 | ViewData["Title"] = "Forgot your password?"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Enter your email.

9 |
10 |
11 |
12 |
13 |
14 |
15 | 16 | 17 | 18 |
19 | 20 |
21 |
22 |
23 | 24 | @section Scripts { 25 | @await Html.PartialAsync("_ValidationScriptsPartial") 26 | } 27 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/Disable2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Disable2faModel 3 | @{ 4 | ViewData["Title"] = "Disable two-factor authentication (2FA)"; 5 | ViewData["ActivePage"] = "TwoFactorAuthentication"; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 | 20 | 21 |
22 |
23 | 24 |
25 |
26 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/GenerateRecoveryCodes.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model GenerateRecoveryCodesModel 3 | @{ 4 | ViewData["Title"] = "Recovery codes"; 5 | ViewData["ActivePage"] = "TwoFactorAuthentication"; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 18 |
19 |
20 | @for (var row = 0; row < Model.RecoveryCodes.Count(); row += 2) 21 | { 22 | @Model.RecoveryCodes[row] @Model.RecoveryCodes[row + 1]
23 | } 24 |
25 |
-------------------------------------------------------------------------------- /DDD.NetCore.Test/DDD.NetCore.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to Development environment will display more detailed information about the error that occurred. 20 |

21 |

22 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 23 |

24 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ResetAuthenticator.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetAuthenticatorModel 3 | @{ 4 | ViewData["Title"] = "Reset authenticator key"; 5 | ViewData["ActivePage"] = "TwoFactorAuthentication"; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 19 |
20 |
21 | 22 |
23 |
-------------------------------------------------------------------------------- /DDD.NetCore.Application/Users/IUserAppService.cs: -------------------------------------------------------------------------------- 1 | using DDD.NetCore.Domain.Authorization; 2 | using DDD.NetCore.Domain.Customers; 3 | using DDD.NetCore.Domain.Uow; 4 | 5 | namespace DDD.NetCore.Application.Users 6 | { 7 | public interface IUserAppService 8 | { 9 | void InitialCustomerForUser(string userId); 10 | 11 | Customer GetCustomerByUserId(string userId); 12 | } 13 | 14 | public class UserAppService : IUserAppService 15 | { 16 | private readonly IUnitOfWork _unitOfWork; 17 | 18 | public UserAppService(IUnitOfWork unitOfWork) 19 | { 20 | _unitOfWork = unitOfWork; 21 | } 22 | 23 | public void InitialCustomerForUser(string userId) 24 | { 25 | throw new System.NotImplementedException(); 26 | } 27 | 28 | public Customer GetCustomerByUserId(string userId) 29 | { 30 | throw new System.NotImplementedException(); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore; 7 | 8 | namespace DDD.NetCore.Web.Data 9 | { 10 | public class ApplicationDbContext : IdentityDbContext 11 | { 12 | public ApplicationDbContext(DbContextOptions options) 13 | : base(options) 14 | { 15 | } 16 | 17 | protected override void OnModelCreating(ModelBuilder builder) 18 | { 19 | base.OnModelCreating(builder); 20 | // Customize the ASP.NET Identity model and override the defaults if needed. 21 | // For example, you can rename the ASP.NET Identity table names and more. 22 | // Add your customizations after calling base.OnModelCreating(builder); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/_LoginPartial.cshtml: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @using DDD.NetCore.Web.Data 3 | @inject SignInManager SignInManager 4 | @inject UserManager UserManager 5 | 6 | @if (SignInManager.IsSignedIn(User)) 7 | { 8 | 18 | } 19 | else 20 | { 21 | 25 | } 26 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Extensions/EmailSenderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text.Encodings.Web; 5 | using System.Threading.Tasks; 6 | using DDD.NetCore.Web.Services; 7 | 8 | namespace DDD.NetCore.Web.Services 9 | { 10 | public static class EmailSenderExtensions 11 | { 12 | public static Task SendEmailConfirmationAsync(this IEmailSender emailSender, string email, string link) 13 | { 14 | return emailSender.SendEmailAsync(email, "Confirm your email", 15 | $"Please confirm your account by clicking here."); 16 | } 17 | 18 | public static Task SendResetPasswordAsync(this IEmailSender emailSender, string email, string callbackUrl) 19 | { 20 | return emailSender.SendEmailAsync(email, "Reset Password", 21 | $"Please reset your password by clicking here."); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "http://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jzaefferer/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.14.0", 31 | "_release": "1.14.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.14.0", 35 | "commit": "c1343fb9823392aa9acbe1c3ffd337b8c92fed48" 36 | }, 37 | "_source": "git://github.com/jzaefferer/jquery-validation.git", 38 | "_target": ">=1.8", 39 | "_originalSource": "jquery-validation" 40 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/LoginWithRecoveryCode.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginWithRecoveryCodeModel 3 | @{ 4 | ViewData["Title"] = "Recovery code verification"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

10 | You have requested to log in with a recovery code. This login will not be remembered until you provide 11 | an authenticator app code at log in or disable 2FA and log in again. 12 |

13 |
14 |
15 |
16 |
17 |
18 | 19 | 20 | 21 |
22 | 23 |
24 |
25 |
26 | 27 | @section Scripts { 28 | @await Html.PartialAsync("_ValidationScriptsPartial") 29 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | using DDD.NetCore.Web.Data; 9 | 10 | namespace DDD.NetCore.Web.Controllers 11 | { 12 | [Route("[controller]/[action]")] 13 | public class AccountController : Controller 14 | { 15 | private readonly SignInManager _signInManager; 16 | private readonly ILogger _logger; 17 | 18 | public AccountController(SignInManager signInManager, ILogger logger) 19 | { 20 | _signInManager = signInManager; 21 | _logger = logger; 22 | } 23 | 24 | [HttpPost] 25 | [ValidateAntiForgeryToken] 26 | public async Task Logout() 27 | { 28 | await _signInManager.SignOutAsync(); 29 | _logger.LogInformation("User logged out."); 30 | return RedirectToPage("/Index"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/DDD.NetCore.Web.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp2.0 4 | aspnet-DDD.NetCore.Web-0E6A0680-1CAB-4355-8B2D-BBAF788B62D8 5 | ..\docker-compose.dcproj 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 3" 33 | }, 34 | "version": "3.3.7", 35 | "_release": "3.3.7", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.7", 39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 40 | }, 41 | "_source": "https://github.com/twbs/bootstrap.git", 42 | "_target": "v3.3.7", 43 | "_originalSource": "bootstrap", 44 | "_direct": true 45 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ExternalLogin.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ExternalLoginModel 3 | @{ 4 | ViewData["Title"] = "Register"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Associate your @Model.LoginProvider account.

9 |
10 | 11 |

12 | You've successfully authenticated with @Model.LoginProvider. 13 | Please enter an email address for this site below and click the Register button to finish 14 | logging in. 15 |

16 | 17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | 25 |
26 | 27 |
28 |
29 |
30 | 31 | @section Scripts { 32 | @await Html.PartialAsync("_ValidationScriptsPartial") 33 | } 34 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Extensions/UrlHelperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace Microsoft.AspNetCore.Mvc 7 | { 8 | public static class UrlHelperExtensions 9 | { 10 | public static string GetLocalUrl(this IUrlHelper urlHelper, string localUrl) 11 | { 12 | if (!urlHelper.IsLocalUrl(localUrl)) 13 | { 14 | return urlHelper.Page("/Index"); 15 | } 16 | 17 | return localUrl; 18 | } 19 | 20 | public static string EmailConfirmationLink(this IUrlHelper urlHelper, string userId, string code, string scheme) 21 | { 22 | return urlHelper.Page( 23 | "/Account/ConfirmEmail", 24 | pageHandler: null, 25 | values: new { userId, code }, 26 | protocol: scheme); 27 | } 28 | 29 | public static string ResetPasswordCallbackLink(this IUrlHelper urlHelper, string userId, string code, string scheme) 30 | { 31 | return urlHelper.Page( 32 | "/Account/ResetPassword", 33 | pageHandler: null, 34 | values: new { userId, code }, 35 | protocol: scheme); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "version": "3.2.6", 4 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 5 | "description": "Add-on to jQuery Validation to enable unobtrusive validation options in data-* attributes.", 6 | "main": [ 7 | "jquery.validate.unobtrusive.js" 8 | ], 9 | "ignore": [ 10 | "**/.*", 11 | "*.json", 12 | "*.md", 13 | "*.txt", 14 | "gulpfile.js" 15 | ], 16 | "keywords": [ 17 | "jquery", 18 | "asp.net", 19 | "mvc", 20 | "validation", 21 | "unobtrusive" 22 | ], 23 | "authors": [ 24 | "Microsoft" 25 | ], 26 | "license": "http://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm", 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/aspnet/jquery-validation-unobtrusive.git" 30 | }, 31 | "dependencies": { 32 | "jquery-validation": ">=1.8", 33 | "jquery": ">=1.8" 34 | }, 35 | "_release": "3.2.6", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.2.6", 39 | "commit": "13386cd1b5947d8a5d23a12b531ce3960be1eba7" 40 | }, 41 | "_source": "git://github.com/aspnet/jquery-validation-unobtrusive.git", 42 | "_target": "3.2.6", 43 | "_originalSource": "jquery-validation-unobtrusive" 44 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/SetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model SetPasswordModel 3 | @{ 4 | ViewData["Title"] = "Set password"; 5 | ViewData["ActivePage"] = "ChangePassword"; 6 | } 7 | 8 |

Set your password

9 | @Html.Partial("_StatusMessage", Model.StatusMessage) 10 |

11 | You do not have a local username/password for this site. Add a local 12 | account so you can log in without an external login. 13 |

14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | @section Scripts { 34 | @await Html.PartialAsync("_ValidationScriptsPartial") 35 | } 36 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ConfirmEmail.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using DDD.NetCore.Web.Data; 9 | 10 | namespace DDD.NetCore.Web.Pages.Account 11 | { 12 | public class ConfirmEmailModel : PageModel 13 | { 14 | private readonly UserManager _userManager; 15 | 16 | public ConfirmEmailModel(UserManager userManager) 17 | { 18 | _userManager = userManager; 19 | } 20 | 21 | public async Task OnGetAsync(string userId, string code) 22 | { 23 | if (userId == null || code == null) 24 | { 25 | return RedirectToPage("/Index"); 26 | } 27 | 28 | var user = await _userManager.FindByIdAsync(userId); 29 | if (user == null) 30 | { 31 | throw new ApplicationException($"Unable to load user with ID '{userId}'."); 32 | } 33 | 34 | var result = await _userManager.ConfirmEmailAsync(user, code); 35 | if (!result.Succeeded) 36 | { 37 | throw new ApplicationException($"Error confirming email for user with ID '{userId}':"); 38 | } 39 | 40 | return Page(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Register.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model RegisterModel 3 | @{ 4 | ViewData["Title"] = "Register"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | 9 |
10 |
11 |
12 |

Create a new account.

13 |
14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 | @section Scripts { 36 | @await Html.PartialAsync("_ValidationScriptsPartial") 37 | } 38 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ChangePassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ChangePasswordModel 3 | @{ 4 | ViewData["Title"] = "Change password"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | @Html.Partial("_StatusMessage", Model.StatusMessage) 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 |
24 | 25 | 26 | 27 |
28 | 29 |
30 |
31 |
32 | 33 | @section Scripts { 34 | @await Html.PartialAsync("_ValidationScriptsPartial") 35 | } 36 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ResetPassword.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ResetPasswordModel 3 | @{ 4 | ViewData["Title"] = "Reset password"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |

Reset your password.

9 |
10 |
11 |
12 |
13 |
14 | 15 |
16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 |
25 |
26 | 27 | 28 | 29 |
30 | 31 |
32 |
33 |
34 | 35 | @section Scripts { 36 | @await Html.PartialAsync("_ValidationScriptsPartial") 37 | } 38 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ManageNavPages.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace DDD.NetCore.Web.Pages.Account.Manage 8 | { 9 | public static class ManageNavPages 10 | { 11 | public static string Index => "Index"; 12 | 13 | public static string ChangePassword => "ChangePassword"; 14 | 15 | public static string ExternalLogins => "ExternalLogins"; 16 | 17 | public static string TwoFactorAuthentication => "TwoFactorAuthentication"; 18 | 19 | public static string IndexNavClass(ViewContext viewContext) => PageNavClass(viewContext, Index); 20 | 21 | public static string ChangePasswordNavClass(ViewContext viewContext) => PageNavClass(viewContext, ChangePassword); 22 | 23 | public static string ExternalLoginsNavClass(ViewContext viewContext) => PageNavClass(viewContext, ExternalLogins); 24 | 25 | public static string TwoFactorAuthenticationNavClass(ViewContext viewContext) => PageNavClass(viewContext, TwoFactorAuthentication); 26 | 27 | public static string PageNavClass(ViewContext viewContext, string page) 28 | { 29 | var activePage = viewContext.ViewData["ActivePage"] as string 30 | ?? System.IO.Path.GetFileNameWithoutExtension(viewContext.ActionDescriptor.DisplayName); 31 | return string.Equals(activePage, page, StringComparison.OrdinalIgnoreCase) ? "active" : null; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Uow/UnitOfWorkServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace DDD.NetCore.Domain.Uow 5 | { 6 | public static class UnitOfWorkServiceCollectionExtensions 7 | { 8 | /// 9 | /// Registers the unit of work given context as a service in the . 10 | /// 11 | /// The type of the db context. 12 | /// The to add services to. 13 | /// The same service collection so that multiple calls can be chained. 14 | /// 15 | /// This method only support one db context, if been called more than once, will throw exception. 16 | /// 17 | public static IServiceCollection AddUnitOfWork(this IServiceCollection services) where TContext : DbContext 18 | { 19 | //services.AddScoped>(); 20 | // Following has a issue: IUnitOfWork cannot support multiple dbcontext/database, 21 | // that means cannot call AddUnitOfWork multiple times. 22 | // Solution: check IUnitOfWork whether or null 23 | services.AddScoped>(); 24 | services.AddScoped, UnitOfWork>(); 25 | 26 | return services; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /DDD.NetCore.Domain/ShoppingCarts/ShoppingCart.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using DDD.NetCore.Domain.Customers; 4 | using DDD.NetCore.Domain.Entities; 5 | 6 | namespace DDD.NetCore.Domain.ShoppingCarts 7 | { 8 | public class ShoppingCart : AggregateRoot 9 | { 10 | public int CustomerId { get; set; } 11 | public virtual Customer Customer { get; set; } 12 | 13 | public List ShoppingCartLines { get; } = new List(); 14 | 15 | //添加 16 | public void AddGoods(Goods.Goods goods, int quantity) 17 | { 18 | ShoppingCartLine line = ShoppingCartLines.FirstOrDefault(p => p.Goods.Id == goods.Id); 19 | if (line == null) 20 | { 21 | ShoppingCartLines.Add(new ShoppingCartLine() 22 | { 23 | Goods = goods, 24 | Qty = quantity, 25 | ShoppingCartId = this.Id 26 | }); 27 | } 28 | else 29 | { 30 | line.Qty += quantity; 31 | } 32 | } 33 | 34 | //点击数量+号或点击数量-号或自己输入一个值 35 | public void ChangeItmeQty(ShoppingCartLine cartLine, int qty) 36 | { 37 | if (qty == 0) 38 | { 39 | RemoveItem(cartLine); 40 | } 41 | 42 | cartLine.Qty = qty; 43 | } 44 | 45 | //移除 46 | public void RemoveItem(ShoppingCartLine cartLine) 47 | { 48 | ShoppingCartLines.Remove(cartLine); 49 | } 50 | 51 | //清空 52 | public void Clear() 53 | { 54 | ShoppingCartLines.Clear(); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/LoginWith2fa.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginWith2faModel 3 | @{ 4 | ViewData["Title"] = "Two-factor authentication"; 5 | } 6 | 7 |

@ViewData["Title"]

8 |
9 |

Your login is protected with an authenticator app. Enter your authenticator code below.

10 |
11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 |
20 |
21 |
22 | 26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 |

35 | Don't have access to your authenticator device? You can 36 | log in with a recovery code. 37 |

38 | 39 | @section Scripts { 40 | @await Html.PartialAsync("_ValidationScriptsPartial") 41 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Orders/SaleOrder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using DDD.NetCore.Domain.Customers; 4 | using DDD.NetCore.Domain.Entities; 5 | 6 | namespace DDD.NetCore.Domain.Orders 7 | { 8 | public class SaleOrder : AggregateRoot 9 | { 10 | public SaleOrder() 11 | { 12 | CreationTime = DateTime.Now; 13 | OrderStatus = SaleOrderStatus.Created; 14 | } 15 | 16 | public int CustomerId { get; set; } 17 | 18 | public virtual Customer Customer { get; set; } 19 | 20 | public SaleOrderStatus OrderStatus { get; set; } 21 | 22 | public int DeliveryAddressId { get; set; } 23 | public virtual Address DeliveryAddress { get; set; } 24 | 25 | public List SaleOrderLines { get; } = new List(); 26 | 27 | public DateTime CreationTime { get; private set; } 28 | 29 | 30 | } 31 | 32 | public enum SaleOrderStatus 33 | { 34 | /// 35 | /// 表示销售订单的已创建状态 - 表明销售订单已被创建(未用)。 36 | /// 37 | Created = 0, 38 | /// 39 | /// 表示销售订单的已付款状态 - 表明客户已向销售订单付款。 40 | /// 41 | Paid, 42 | /// 43 | /// 表示销售订单的已拣货状态 - 表明销售订单中包含的商品已从仓库拣货(未用)。 44 | /// 45 | Picked, 46 | /// 47 | /// 表示销售订单的已发货状态。 48 | /// 49 | Dispatched, 50 | /// 51 | /// 表示销售订单的已派送状态。 52 | /// 53 | Delivered 54 | } 55 | 56 | public class SaleOrderLine : Entity 57 | { 58 | public int SaleOrderId { get; set; } 59 | public virtual SaleOrder SaleOrder { get; set; } 60 | public int GoodsId { get; set; } 61 | public virtual Goods.Goods Goods { get; set; } 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/GenerateRecoveryCodes.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account.Manage 12 | { 13 | public class GenerateRecoveryCodesModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly ILogger _logger; 17 | 18 | public GenerateRecoveryCodesModel( 19 | UserManager userManager, 20 | ILogger logger) 21 | { 22 | _userManager = userManager; 23 | _logger = logger; 24 | } 25 | 26 | public string[] RecoveryCodes { get; set; } 27 | 28 | public async Task OnGetAsync() 29 | { 30 | var user = await _userManager.GetUserAsync(User); 31 | if (user == null) 32 | { 33 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 34 | } 35 | 36 | if (!user.TwoFactorEnabled) 37 | { 38 | throw new ApplicationException($"Cannot generate recovery codes for user with ID '{user.Id}' as they do not have 2FA enabled."); 39 | } 40 | 41 | var recoveryCodes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 10); 42 | RecoveryCodes = recoveryCodes.ToArray(); 43 | 44 | _logger.LogInformation("User with ID '{UserId}' has generated new 2FA recovery codes.", user.Id); 45 | 46 | return Page(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/TwoFactorAuthentication.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model TwoFactorAuthenticationModel 3 | @{ 4 | ViewData["Title"] = "Two-factor authentication (2FA)"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | @if (Model.Is2faEnabled) 9 | { 10 | if (Model.RecoveryCodesLeft == 0) 11 | { 12 |
13 | You have no recovery codes left. 14 |

You must generate a new set of recovery codes before you can log in with a recovery code.

15 |
16 | } 17 | else if (Model.RecoveryCodesLeft == 1) 18 | { 19 |
20 | You have 1 recovery code left. 21 |

You can generate a new set of recovery codes.

22 |
23 | } 24 | else if (Model.RecoveryCodesLeft <= 3) 25 | { 26 |
27 | You have @Model.RecoveryCodesLeft recovery codes left. 28 |

You should generate a new set of recovery codes.

29 |
30 | } 31 | 32 | Disable 2FA 33 | Reset recovery codes 34 | } 35 | 36 |
Authenticator app
37 | @if (!Model.HasAuthenticator) 38 | { 39 | Add authenticator app 40 | } 41 | else 42 | { 43 | Configure authenticator app 44 | Reset authenticator app 45 | } 46 | 47 | @section Scripts { 48 | @await Html.PartialAsync("_ValidationScriptsPartial") 49 | } 50 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ResetAuthenticator.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account.Manage 12 | { 13 | public class ResetAuthenticatorModel : PageModel 14 | { 15 | UserManager _userManager; 16 | ILogger _logger; 17 | 18 | public ResetAuthenticatorModel( 19 | UserManager userManager, 20 | ILogger logger) 21 | { 22 | _userManager = userManager; 23 | _logger = logger; 24 | } 25 | public async Task OnGet() 26 | { 27 | var user = await _userManager.GetUserAsync(User); 28 | if (user == null) 29 | { 30 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 31 | } 32 | 33 | return Page(); 34 | } 35 | 36 | public async Task OnPostAsync() 37 | { 38 | var user = await _userManager.GetUserAsync(User); 39 | if (user == null) 40 | { 41 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 42 | } 43 | 44 | await _userManager.SetTwoFactorEnabledAsync(user, false); 45 | await _userManager.ResetAuthenticatorKeyAsync(user); 46 | _logger.LogInformation("User with ID '{UserId}' has reset their authentication app key.", user.Id); 47 | 48 | return RedirectToPage("./EnableAuthenticator"); 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Profile"; 5 | } 6 | 7 |

@ViewData["Title"]

8 | @Html.Partial("_StatusMessage", Model.StatusMessage) 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 |
17 |
18 | 19 | @if (Model.IsEmailConfirmed) 20 | { 21 |
22 | 23 | 24 |
25 | } 26 | else 27 | { 28 | 29 | 30 | } 31 | 32 |
33 |
34 | 35 | 36 | 37 |
38 | 39 |
40 |
41 |
42 | 43 | @section Scripts { 44 | @await Html.PartialAsync("_ValidationScriptsPartial") 45 | } 46 | -------------------------------------------------------------------------------- /DDD.NetCore.Test/TestData/GoodsTestData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using DDD.NetCore.Domain.Goods; 6 | using DDD.NetCore.Infrastructure.EfCore; 7 | 8 | namespace DDD.NetCore.Test.TestData 9 | { 10 | public class GoodsTestData 11 | { 12 | public static void Initialize(ApplicationDbContext context) 13 | { 14 | if (context.Goods.Any()) 15 | { 16 | return; ; 17 | } 18 | var mobileCategory = new GoodsCategory() { Name = "手机" }; 19 | var tvGoodsCategoryCategory = new GoodsCategory() { Name = "电视" }; 20 | var computerCategory = new GoodsCategory() { Name = "电脑" }; 21 | 22 | context.GoodsCategories.Add(mobileCategory); 23 | context.GoodsCategories.Add(tvGoodsCategoryCategory); 24 | context.GoodsCategories.Add(computerCategory); 25 | context.SaveChanges(); 26 | 27 | context.Goods.AddRange( 28 | new Goods { GoodsCategoryId = mobileCategory.Id, Name = "iphone5s 64G", Description = "iphone5s 64G 黑色", Price = 3000, Stock = 10 }, 29 | new Goods { GoodsCategoryId = mobileCategory.Id, Name = "iphone5s 128G", Description = "iphone5s 128G 黑色", Price = 3999, Stock = 10 }, 30 | new Goods { GoodsCategoryId = mobileCategory.Id, Name = "iphone6 64G", Description = "iphone6 64G 白色", Price = 4299, Stock = 10 }, 31 | new Goods { GoodsCategoryId = mobileCategory.Id, Name = "iphone6s 128G", Description = "iphone6s 128G 黑色", Price = 4999, Stock = 10 }, 32 | new Goods { GoodsCategoryId = mobileCategory.Id, Name = "iphone7 64G", Description = "iphone7 64G 银色", Price = 4599, Stock = 10 }, 33 | new Goods { GoodsCategoryId = mobileCategory.Id, Name = "iphone7 128G", Description = "iphone7 128G 银色", Price = 5299, Stock = 10 } 34 | ); 35 | context.SaveChanges(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /DDD.NetCore.Domain/Customers/Address.cs: -------------------------------------------------------------------------------- 1 | using DDD.NetCore.Domain.Entities; 2 | 3 | namespace DDD.NetCore.Domain.Customers 4 | { 5 | /// 6 | /// 地址 7 | /// 8 | public class Address : Entity 9 | { 10 | public Address(string province, string city, string county, 11 | string street, string contactRealName, string contactPhone, string zip = "") 12 | { 13 | Province = province; 14 | City = city; 15 | County = county; 16 | Street = street; 17 | ContactRealName = contactRealName; 18 | ContactPhone = contactPhone; 19 | Zip = zip; 20 | } 21 | 22 | /// 23 | /// 联系人姓名 24 | /// 25 | public string ContactRealName { get; private set; } 26 | 27 | /// 28 | /// 联系电话 29 | /// 30 | public string ContactPhone { get; private set; } 31 | 32 | /// 33 | /// 省份 34 | /// 35 | public string Province { get; private set; } 36 | 37 | /// 38 | /// 城市 39 | /// 40 | public string City { get; private set; } 41 | 42 | /// 43 | /// 区县 44 | /// 45 | public string County { get; private set; } 46 | 47 | /// 48 | /// 街道 49 | /// 50 | public string Street { get; private set; } 51 | 52 | /// 53 | /// 邮政编码 54 | /// 55 | public string Zip { get; private set; } 56 | 57 | /// 58 | /// 省市区街道 59 | /// 60 | public string SimpleAddress => $"{Province} {City} {County} {Street}"; 61 | 62 | /// 63 | /// 市区街道详情 64 | /// 65 | public string DetailAddress => $"{County}{Street}({Zip})"; 66 | 67 | public bool IsDefault { get; set; } 68 | 69 | 70 | } 71 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/TwoFactorAuthentication.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account.Manage 12 | { 13 | public class TwoFactorAuthenticationModel : PageModel 14 | { 15 | private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}"; 16 | 17 | private readonly UserManager _userManager; 18 | private readonly SignInManager _signInManager; 19 | private readonly ILogger _logger; 20 | 21 | public TwoFactorAuthenticationModel( 22 | UserManager userManager, 23 | SignInManager signInManager, 24 | ILogger logger) 25 | { 26 | _userManager = userManager; 27 | _signInManager = signInManager; 28 | _logger = logger; 29 | } 30 | 31 | public bool HasAuthenticator { get; set; } 32 | 33 | public int RecoveryCodesLeft { get; set; } 34 | 35 | [BindProperty] 36 | public bool Is2faEnabled { get; set; } 37 | 38 | public async Task OnGet() 39 | { 40 | var user = await _userManager.GetUserAsync(User); 41 | if (user == null) 42 | { 43 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 44 | } 45 | 46 | HasAuthenticator = await _userManager.GetAuthenticatorKeyAsync(user) != null; 47 | Is2faEnabled = await _userManager.GetTwoFactorEnabledAsync(user); 48 | RecoveryCodesLeft = await _userManager.CountRecoveryCodesAsync(user); 49 | 50 | return Page(); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ExternalLogins.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ExternalLoginsModel 3 | @{ 4 | ViewData["Title"] = "Manage your external logins"; 5 | } 6 | 7 | @Html.Partial("_StatusMessage", Model.StatusMessage) 8 | @if (Model.CurrentLogins?.Count > 0) 9 | { 10 |

Registered Logins

11 | 12 | 13 | @foreach (var login in Model.CurrentLogins) 14 | { 15 | 16 | 17 | 33 | 34 | } 35 | 36 |
@login.LoginProvider 18 | @if (Model.ShowRemoveButton) 19 | { 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | } 28 | else 29 | { 30 | @:   31 | } 32 |
37 | } 38 | @if (Model.OtherLogins?.Count > 0) 39 | { 40 |

Add another service to log in.

41 |
42 |
43 |
44 |

45 | @foreach (var provider in Model.OtherLogins) 46 | { 47 | 48 | } 49 |

50 |
51 |
52 | } 53 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ForgotPassword.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using DDD.NetCore.Web.Data; 9 | using DDD.NetCore.Web.Services; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account 12 | { 13 | public class ForgotPasswordModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly IEmailSender _emailSender; 17 | 18 | public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) 19 | { 20 | _userManager = userManager; 21 | _emailSender = emailSender; 22 | } 23 | 24 | [BindProperty] 25 | public InputModel Input { get; set; } 26 | 27 | public class InputModel 28 | { 29 | [Required] 30 | [EmailAddress] 31 | public string Email { get; set; } 32 | } 33 | 34 | public async Task OnPostAsync() 35 | { 36 | if (ModelState.IsValid) 37 | { 38 | var user = await _userManager.FindByEmailAsync(Input.Email); 39 | if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) 40 | { 41 | // Don't reveal that the user does not exist or is not confirmed 42 | return RedirectToPage("./ForgotPasswordConfirmation"); 43 | } 44 | 45 | // For more information on how to enable account confirmation and password reset please 46 | // visit https://go.microsoft.com/fwlink/?LinkID=532713 47 | var code = await _userManager.GeneratePasswordResetTokenAsync(user); 48 | var callbackUrl = Url.ResetPasswordCallbackLink(user.Id, code, Request.Scheme); 49 | await _emailSender.SendResetPasswordAsync(Input.Email, callbackUrl); 50 | return RedirectToPage("./ForgotPasswordConfirmation"); 51 | } 52 | 53 | return Page(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /DDD.NetCore/Exception/EntityNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace DDD.NetCore.Exception 4 | { 5 | /// 6 | /// This exception is thrown if an entity excepted to be found but not found. 7 | /// 8 | public class EntityNotFoundException : ExceptionBase 9 | { 10 | /// 11 | /// Type of the entity. 12 | /// 13 | public Type EntityType { get; set; } 14 | 15 | /// 16 | /// Id of the Entity. 17 | /// 18 | public object Id { get; set; } 19 | 20 | /// 21 | /// Creates a new object. 22 | /// 23 | public EntityNotFoundException() 24 | { 25 | 26 | } 27 | 28 | /// 29 | /// Creates a new object. 30 | /// 31 | public EntityNotFoundException(Type entityType, object id) 32 | : this(entityType, id, null) 33 | { 34 | 35 | } 36 | 37 | /// 38 | /// Creates a new object. 39 | /// 40 | public EntityNotFoundException(Type entityType, object id, System.Exception innerException) 41 | : base($"There is no such an entity. Entity type: {entityType.FullName}, id: {id}", innerException) 42 | { 43 | EntityType = entityType; 44 | Id = id; 45 | } 46 | 47 | /// 48 | /// Creates a new object. 49 | /// 50 | /// Exception message 51 | public EntityNotFoundException(string message) 52 | : base(message) 53 | { 54 | 55 | } 56 | 57 | /// 58 | /// Creates a new object. 59 | /// 60 | /// Exception message 61 | /// Inner exception 62 | public EntityNotFoundException(string message, System.Exception innerException) 63 | : base(message, innerException) 64 | { 65 | 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /DDD.NetCore.Application/ShoppingCarts/ShoppingCartService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DDD.NetCore.Domain.Customers; 4 | using DDD.NetCore.Domain.Goods; 5 | using DDD.NetCore.Domain.ShoppingCarts; 6 | 7 | namespace DDD.NetCore.Application.ShoppingCarts 8 | { 9 | public class ShoppingCartService : IShoppingCartService 10 | { 11 | private readonly IShoppingCartRepository _shoppingCartRepository; 12 | private readonly IGoodsRepository _goodsRepository; 13 | private readonly ICustomerRepository _customerRepository; 14 | public ShoppingCartService(IShoppingCartRepository shoppingCartRepository, 15 | IGoodsRepository goodsRepository, ICustomerRepository customerRepository) 16 | { 17 | _shoppingCartRepository = shoppingCartRepository; 18 | _goodsRepository = goodsRepository; 19 | _customerRepository = customerRepository; 20 | } 21 | public void AddGoodsToCart(string userId, int goodsId, int qty) 22 | { 23 | var customer = _customerRepository.GetCustomerByUserId(userId); 24 | var cart = _shoppingCartRepository.Find(customer.ShoppingCartId); 25 | var goods = _goodsRepository.Find(goodsId); 26 | 27 | cart.AddGoods(goods, qty); 28 | _shoppingCartRepository.Update(cart); 29 | 30 | } 31 | 32 | public void RemoveItemFromCart(int cartId, int cartLineId) 33 | { 34 | var cart = _shoppingCartRepository.Find(cartId); 35 | var cartLine = cart.ShoppingCartLines.FirstOrDefault(c => c.Id == cartLineId); 36 | cart.RemoveItem(cartLine); 37 | } 38 | 39 | public ShoppingCart GetShoppingCart(int cartId) 40 | { 41 | return _shoppingCartRepository.Find(cartId); 42 | } 43 | 44 | public void ChangeItmeQty(int cartId, int cartLineId, int qty) 45 | { 46 | var cart = _shoppingCartRepository.Find(cartId); 47 | var cartLine = cart.ShoppingCartLines.FirstOrDefault(c => c.Id == cartLineId); 48 | cart.ChangeItmeQty(cartLine, qty); 49 | } 50 | 51 | public void ClearCart(int cartId) 52 | { 53 | var cart = _shoppingCartRepository.Find(cartId); 54 | cart.Clear(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/Disable2fa.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Identity; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account.Manage 12 | { 13 | public class Disable2faModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly ILogger _logger; 17 | 18 | public Disable2faModel( 19 | UserManager userManager, 20 | ILogger logger) 21 | { 22 | _userManager = userManager; 23 | _logger = logger; 24 | } 25 | 26 | public async Task OnGet() 27 | { 28 | var user = await _userManager.GetUserAsync(User); 29 | if (user == null) 30 | { 31 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 32 | } 33 | 34 | if (!await _userManager.GetTwoFactorEnabledAsync(user)) 35 | { 36 | throw new ApplicationException($"Cannot disable 2FA for user with ID '{_userManager.GetUserId(User)}' as it's not currently enabled."); 37 | } 38 | 39 | return Page(); 40 | } 41 | 42 | public async Task OnPostAsync() 43 | { 44 | var user = await _userManager.GetUserAsync(User); 45 | if (user == null) 46 | { 47 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 48 | } 49 | 50 | var disable2faResult = await _userManager.SetTwoFactorEnabledAsync(user, false); 51 | if (!disable2faResult.Succeeded) 52 | { 53 | throw new ApplicationException($"Unexpected error occurred disabling 2FA for user with ID '{_userManager.GetUserId(User)}'."); 54 | } 55 | 56 | _logger.LogInformation("User with ID '{UserId}' has disabled 2fa.", _userManager.GetUserId(User)); 57 | 58 | return RedirectToPage("./TwoFactorAuthentication"); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /DDD.NetCore.Test/TestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DDD.NetCore.Infrastructure.EfCore; 3 | using DDD.NetCore.Test.TestData; 4 | using Microsoft.Data.Sqlite; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.EntityFrameworkCore.Diagnostics; 7 | using Microsoft.Extensions.Options; 8 | 9 | namespace DDD.NetCore.Test 10 | { 11 | public class TestBase 12 | { 13 | /// 14 | /// 内存数据库,用于测试关系型数据库 15 | /// 16 | public ApplicationDbContext InMemoryTestDbContext { get; set; } 17 | 18 | /// 19 | /// 内存数据库,用于测试非关系型数据库 20 | /// 21 | public ApplicationDbContext InMemorySqliteTestDbContext { get; set; } 22 | 23 | 24 | public TestBase() 25 | { 26 | GetInMemoryDbContext(); 27 | GetSqliteInMemoryDbContext(); 28 | } 29 | 30 | private ApplicationDbContext GetInMemoryDbContext() 31 | { 32 | var options = new DbContextOptionsBuilder() 33 | .UseInMemoryDatabase(Guid.NewGuid().ToString()) 34 | // don't raise the error warning us that the in memory db doesn't support transactions 35 | .ConfigureWarnings(x => x.Ignore(InMemoryEventId.TransactionIgnoredWarning)) 36 | .Options; 37 | 38 | InMemoryTestDbContext = new ApplicationDbContext(options); 39 | GoodsTestData.Initialize(InMemoryTestDbContext); 40 | UsersTestData.CreateTestUser(InMemoryTestDbContext); 41 | return InMemoryTestDbContext; 42 | } 43 | 44 | public ApplicationDbContext GetSqliteInMemoryDbContext() 45 | { 46 | // In-memory database only exists while the connection is open 47 | var connection = new SqliteConnection("DataSource=:memory:"); 48 | connection.Open(); 49 | var options = new DbContextOptionsBuilder() 50 | .UseSqlite(connection) 51 | .Options; 52 | 53 | InMemorySqliteTestDbContext = new ApplicationDbContext(options); 54 | InMemorySqliteTestDbContext.Database.EnsureCreated(); 55 | 56 | GoodsTestData.Initialize(InMemorySqliteTestDbContext); 57 | UsersTestData.CreateTestUser(InMemorySqliteTestDbContext); 58 | 59 | return InMemorySqliteTestDbContext; 60 | 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Uow/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using DDD.NetCore.Domain.Entities; 3 | using DDD.NetCore.Domain.Repositories; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Storage; 6 | 7 | namespace DDD.NetCore.Domain.Uow 8 | { 9 | public class UnitOfWork : IUnitOfWork where TDbContext : DbContext 10 | { 11 | private bool _disposed = false; 12 | 13 | private IDbContextTransaction _transaction; 14 | 15 | private IServiceProvider _serviceProvider; 16 | public UnitOfWork(TDbContext dbContext,IServiceProvider serviceProvider) 17 | { 18 | DbContext = dbContext; 19 | _serviceProvider = serviceProvider; 20 | } 21 | 22 | 23 | public IRepository GetRepository() where TEntity : class, IEntity 24 | { 25 | var repository = GetRepository() as IRepository; 26 | return repository; 27 | } 28 | 29 | public IRepository GetRepository() where TEntity : class, IEntity 30 | { 31 | //return _serviceProvider.GetService>(); 32 | return new EfCoreRepository(DbContext); 33 | } 34 | 35 | public void SaveChanges() 36 | { 37 | DbContext.SaveChanges(); 38 | } 39 | 40 | public IDbContextTransaction BeginTransaction() 41 | { 42 | _transaction = DbContext.Database.BeginTransaction(); 43 | return _transaction; 44 | } 45 | 46 | public TDbContext DbContext { get; } 47 | 48 | /// 49 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 50 | /// 51 | public void Dispose() 52 | { 53 | Dispose(true); 54 | 55 | GC.SuppressFinalize(this); 56 | } 57 | 58 | /// 59 | /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. 60 | /// 61 | /// The disposing. 62 | protected virtual void Dispose(bool disposing) 63 | { 64 | if (!_disposed) 65 | { 66 | if (disposing) 67 | { 68 | _transaction.Dispose(); 69 | DbContext.Dispose(); 70 | } 71 | } 72 | 73 | _disposed = true; 74 | } 75 | 76 | } 77 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/EnableAuthenticator.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model EnableAuthenticatorModel 3 | @{ 4 | ViewData["Title"] = "Configure authenticator app"; 5 | ViewData["ActivePage"] = "TwoFactorAuthentication"; 6 | } 7 | 8 |

@ViewData["Title"]

9 |
10 |

To use an authenticator app go through the following steps:

11 |
    12 |
  1. 13 |

    14 | Download a two-factor authenticator app like Microsoft Authenticator for 15 | Windows Phone, 16 | Android and 17 | iOS or 18 | Google Authenticator for 19 | Android and 20 | iOS. 21 |

    22 |
  2. 23 |
  3. 24 |

    Scan the QR Code or enter this key @Model.SharedKey into your two factor authenticator app. Spaces and casing do not matter.

    25 |
    To enable QR code generation please read our documentation.
    26 |
    27 |
    28 |
  4. 29 |
  5. 30 |

    31 | Once you have scanned the QR code or input the key above, your two factor authentication app will provide you 32 | with a unique code. Enter the code in the confirmation box below. 33 |

    34 |
    35 |
    36 |
    37 |
    38 | 39 | 40 | 41 |
    42 | 43 |
    44 |
    45 |
    46 |
    47 |
  6. 48 |
49 |
50 | 51 | @section Scripts { 52 | @await Html.PartialAsync("_ValidationScriptsPartial") 53 | } 54 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Hosting; 9 | using Microsoft.EntityFrameworkCore; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using DDD.NetCore.Web.Data; 13 | using DDD.NetCore.Web.Services; 14 | 15 | namespace DDD.NetCore.Web 16 | { 17 | public class Startup 18 | { 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | } 23 | 24 | public IConfiguration Configuration { get; } 25 | 26 | // This method gets called by the runtime. Use this method to add services to the container. 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | services.AddDbContext(options => 30 | options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); 31 | 32 | services.AddIdentity() 33 | .AddEntityFrameworkStores() 34 | .AddDefaultTokenProviders(); 35 | 36 | services.AddMvc() 37 | .AddRazorPagesOptions(options => 38 | { 39 | options.Conventions.AuthorizeFolder("/Account/Manage"); 40 | options.Conventions.AuthorizePage("/Account/Logout"); 41 | }); 42 | 43 | // Register no-op EmailSender used by account confirmation and password reset during development 44 | // For more information on how to enable account confirmation and password reset please visit https://go.microsoft.com/fwlink/?LinkID=532713 45 | services.AddSingleton(); 46 | } 47 | 48 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 49 | public void Configure(IApplicationBuilder app, IHostingEnvironment env) 50 | { 51 | if (env.IsDevelopment()) 52 | { 53 | app.UseDeveloperExceptionPage(); 54 | app.UseBrowserLink(); 55 | app.UseDatabaseErrorPage(); 56 | } 57 | else 58 | { 59 | app.UseExceptionHandler("/Error"); 60 | } 61 | 62 | app.UseStaticFiles(); 63 | 64 | app.UseAuthentication(); 65 | 66 | app.UseMvc(routes => 67 | { 68 | routes.MapRoute( 69 | name: "default", 70 | template: "{controller}/{action=Index}/{id?}"); 71 | }); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /DDD.NetCore.Infrastructure/EfCore/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using DDD.NetCore.Domain.Authorization; 2 | using DDD.NetCore.Domain.Customers; 3 | using DDD.NetCore.Domain.Goods; 4 | using DDD.NetCore.Domain.Orders; 5 | using DDD.NetCore.Domain.ShoppingCarts; 6 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DDD.NetCore.Infrastructure.EfCore 10 | { 11 | public class ApplicationDbContext : IdentityDbContext 12 | { 13 | public ApplicationDbContext(DbContextOptions options) 14 | : base(options) 15 | { 16 | } 17 | //private readonly DbConnection _dbConnection; 18 | //public ApplicationDbContext(DbConnection dbConnection) 19 | //{ 20 | // _dbConnection = dbConnection; 21 | //} 22 | 23 | public DbSet Customers { get; set; } 24 | public DbSet Goods { get; set; } 25 | public DbSet GoodsCategories { get; set; } 26 | public DbSet ShoppingCarts { get; set; } 27 | public DbSet ShoppingCartLines { get; set; } 28 | 29 | public DbSet
Addresses { get; set; } 30 | 31 | public DbSet SaleOrders { get; set; } 32 | 33 | public DbSet SaleOrderLines { get; set; } 34 | 35 | //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 36 | //{ 37 | // optionsBuilder.UseSqlServer(_dbConnection); 38 | //} 39 | 40 | protected override void OnModelCreating(ModelBuilder builder) 41 | { 42 | base.OnModelCreating(builder); 43 | // Customize the ASP.NET Identity model and override the defaults if needed. 44 | // For example, you can rename the ASP.NET Identity table names and more. 45 | // Add your customizations after calling base.OnModelCreating(builder); 46 | builder.Entity().HasOne(c => c.ApplicationUser); 47 | builder.Entity() 48 | .HasOne(c => c.ShoppingCart) 49 | .WithOne(sc => sc.Customer) 50 | .HasForeignKey(c => c.CustomerId); 51 | 52 | builder.Entity().HasMany(c => c.ShippingAddresses); 53 | builder.Entity().HasMany(sc => sc.ShoppingCartLines).WithOne(cl => cl.ShoppingCart); 54 | builder.Entity().HasOne(scl => scl.Goods); 55 | 56 | builder.Entity().HasOne(g => g.GoodsCategory).WithMany(gc => gc.GoodsList); 57 | 58 | builder.Entity().HasMany(so => so.SaleOrderLines).WithOne(sol => sol.SaleOrder); 59 | builder.Entity().HasOne(so => so.Customer); 60 | builder.Entity().HasOne(so => so.DeliveryAddress); 61 | 62 | builder.Entity().HasOne(sol => sol.Goods); 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ResetPassword.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account 12 | { 13 | public class ResetPasswordModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | 17 | public ResetPasswordModel(UserManager userManager) 18 | { 19 | _userManager = userManager; 20 | } 21 | 22 | [BindProperty] 23 | public InputModel Input { get; set; } 24 | 25 | public class InputModel 26 | { 27 | [Required] 28 | [EmailAddress] 29 | public string Email { get; set; } 30 | 31 | [Required] 32 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 33 | [DataType(DataType.Password)] 34 | public string Password { get; set; } 35 | 36 | [DataType(DataType.Password)] 37 | [Display(Name = "Confirm password")] 38 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 39 | public string ConfirmPassword { get; set; } 40 | 41 | public string Code { get; set; } 42 | } 43 | 44 | public IActionResult OnGet(string code = null) 45 | { 46 | if (code == null) 47 | { 48 | throw new ApplicationException("A code must be supplied for password reset."); 49 | } 50 | else 51 | { 52 | Input = new InputModel 53 | { 54 | Code = code 55 | }; 56 | return Page(); 57 | } 58 | } 59 | 60 | public async Task OnPostAsync() 61 | { 62 | if (!ModelState.IsValid) 63 | { 64 | return Page(); 65 | } 66 | 67 | var user = await _userManager.FindByEmailAsync(Input.Email); 68 | if (user == null) 69 | { 70 | // Don't reveal that the user does not exist 71 | return RedirectToPage("./ResetPasswordConfirmation"); 72 | } 73 | 74 | var result = await _userManager.ResetPasswordAsync(user, Input.Code, Input.Password); 75 | if (result.Succeeded) 76 | { 77 | return RedirectToPage("./ResetPasswordConfirmation"); 78 | } 79 | 80 | foreach (var error in result.Errors) 81 | { 82 | ModelState.AddModelError(string.Empty, error.Description); 83 | } 84 | return Page(); 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/LoginWithRecoveryCode.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.Extensions.Logging; 10 | using DDD.NetCore.Web.Data; 11 | 12 | namespace DDD.NetCore.Web.Pages.Account 13 | { 14 | public class LoginWithRecoveryCodeModel : PageModel 15 | { 16 | private readonly SignInManager _signInManager; 17 | private readonly ILogger _logger; 18 | 19 | public LoginWithRecoveryCodeModel(SignInManager signInManager, ILogger logger) 20 | { 21 | _signInManager = signInManager; 22 | _logger = logger; 23 | } 24 | 25 | [BindProperty] 26 | public InputModel Input { get; set; } 27 | 28 | public string ReturnUrl { get; set; } 29 | 30 | public class InputModel 31 | { 32 | [BindProperty] 33 | [Required] 34 | [DataType(DataType.Text)] 35 | [Display(Name = "Recovery Code")] 36 | public string RecoveryCode { get; set; } 37 | } 38 | 39 | public async Task OnGetAsync(string returnUrl = null) 40 | { 41 | // Ensure the user has gone through the username & password screen first 42 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 43 | if (user == null) 44 | { 45 | throw new ApplicationException($"Unable to load two-factor authentication user."); 46 | } 47 | 48 | ReturnUrl = returnUrl; 49 | 50 | return Page(); 51 | } 52 | 53 | public async Task OnPostAsync(string returnUrl = null) 54 | { 55 | if (!ModelState.IsValid) 56 | { 57 | return Page(); 58 | } 59 | 60 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 61 | if (user == null) 62 | { 63 | throw new ApplicationException($"Unable to load two-factor authentication user."); 64 | } 65 | 66 | var recoveryCode = Input.RecoveryCode.Replace(" ", string.Empty); 67 | 68 | var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(recoveryCode); 69 | 70 | if (result.Succeeded) 71 | { 72 | _logger.LogInformation("User with ID '{UserId}' logged in with a recovery code.", user.Id); 73 | return LocalRedirect(Url.GetLocalUrl(returnUrl)); 74 | } 75 | if (result.IsLockedOut) 76 | { 77 | _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); 78 | return RedirectToPage("./Lockout"); 79 | } 80 | else 81 | { 82 | _logger.LogWarning("Invalid recovery code entered for user with ID '{UserId}' ", user.Id); 83 | ModelState.AddModelError(string.Empty, "Invalid recovery code entered."); 84 | return Page(); 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/SetPassword.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account.Manage 12 | { 13 | public class SetPasswordModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly SignInManager _signInManager; 17 | 18 | public SetPasswordModel( 19 | UserManager userManager, 20 | SignInManager signInManager) 21 | { 22 | _userManager = userManager; 23 | _signInManager = signInManager; 24 | } 25 | 26 | [BindProperty] 27 | public InputModel Input { get; set; } 28 | 29 | [TempData] 30 | public string StatusMessage { get; set; } 31 | 32 | public class InputModel 33 | { 34 | [Required] 35 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 36 | [DataType(DataType.Password)] 37 | [Display(Name = "New password")] 38 | public string NewPassword { get; set; } 39 | 40 | [DataType(DataType.Password)] 41 | [Display(Name = "Confirm new password")] 42 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 43 | public string ConfirmPassword { get; set; } 44 | } 45 | 46 | public async Task OnGetAsync() 47 | { 48 | var user = await _userManager.GetUserAsync(User); 49 | if (user == null) 50 | { 51 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 52 | } 53 | 54 | var hasPassword = await _userManager.HasPasswordAsync(user); 55 | 56 | if (hasPassword) 57 | { 58 | return RedirectToPage("./ChangePassword"); 59 | } 60 | 61 | return Page(); 62 | } 63 | 64 | public async Task OnPostAsync() 65 | { 66 | if (!ModelState.IsValid) 67 | { 68 | return Page(); 69 | } 70 | 71 | var user = await _userManager.GetUserAsync(User); 72 | if (user == null) 73 | { 74 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 75 | } 76 | 77 | var addPasswordResult = await _userManager.AddPasswordAsync(user, Input.NewPassword); 78 | if (!addPasswordResult.Succeeded) 79 | { 80 | foreach (var error in addPasswordResult.Errors) 81 | { 82 | ModelState.AddModelError(string.Empty, error.Description); 83 | } 84 | return Page(); 85 | } 86 | 87 | await _signInManager.SignInAsync(user, isPersistent: false); 88 | StatusMessage = "Your password has been set."; 89 | 90 | return RedirectToPage(); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /DDD.NetCore.Test/UnitTest1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using DDD.NetCore.Domain.Customers; 4 | using DDD.NetCore.Domain.ShoppingCarts; 5 | using Microsoft.EntityFrameworkCore; 6 | using Xunit; 7 | 8 | namespace DDD.NetCore.Test 9 | { 10 | public class UnitTest1 : TestBase 11 | { 12 | [Fact] 13 | public void Test1() 14 | { 15 | var result = InMemorySqliteTestDbContext.Database.EnsureCreated(); 16 | Assert.Equal(true, InMemorySqliteTestDbContext.Goods.Any()); 17 | 18 | } 19 | 20 | [Fact] 21 | public void Test2() 22 | { 23 | var customerCount = InMemorySqliteTestDbContext.Customers.Count(); 24 | var shoppingCartCount = InMemorySqliteTestDbContext.ShoppingCarts.Count(); 25 | using (var trasnaction = InMemorySqliteTestDbContext.Database.BeginTransaction()) 26 | { 27 | var user = InMemorySqliteTestDbContext.Users.FirstOrDefault(); 28 | var customer = new Customer() { ApplicationUserId = user.Id }; 29 | InMemorySqliteTestDbContext.Customers.Add(customer); 30 | InMemorySqliteTestDbContext.SaveChanges(); 31 | 32 | var cart = new ShoppingCart() { CustomerId = customer.Id }; 33 | InMemorySqliteTestDbContext.ShoppingCarts.Add(cart); 34 | InMemorySqliteTestDbContext.SaveChanges(); 35 | 36 | customer.ShoppingCartId = cart.Id; 37 | InMemorySqliteTestDbContext.Entry(customer).State = EntityState.Modified; 38 | InMemorySqliteTestDbContext.SaveChanges(); 39 | 40 | trasnaction.Commit(); 41 | } 42 | Assert.Equal(customerCount + 1, InMemorySqliteTestDbContext.Customers.Count()); 43 | Assert.Equal(shoppingCartCount + 1, InMemorySqliteTestDbContext.ShoppingCarts.Count()); 44 | } 45 | 46 | [Fact] 47 | public void Test3() 48 | { 49 | var customerCount = InMemorySqliteTestDbContext.Customers.Count(); 50 | var shoppingCartCount = InMemorySqliteTestDbContext.ShoppingCarts.Count(); 51 | using (var trasnaction = InMemorySqliteTestDbContext.Database.BeginTransaction()) 52 | { 53 | try 54 | { 55 | var user = InMemorySqliteTestDbContext.Users.FirstOrDefault(); 56 | var customer = new Customer() { ApplicationUserId = user.Id }; 57 | InMemorySqliteTestDbContext.Customers.Add(customer); 58 | InMemorySqliteTestDbContext.SaveChanges(); 59 | 60 | var cart = new ShoppingCart();//{ CustomerId = customer.Id }; 61 | InMemorySqliteTestDbContext.ShoppingCarts.Add(cart); 62 | InMemorySqliteTestDbContext.SaveChanges(); 63 | 64 | customer.ShoppingCartId = cart.Id; 65 | InMemorySqliteTestDbContext.Entry(customer).State = EntityState.Modified; 66 | InMemorySqliteTestDbContext.SaveChanges(); 67 | 68 | trasnaction.Commit(); 69 | } 70 | catch (System.Exception) 71 | { 72 | trasnaction.Rollback(); 73 | } 74 | } 75 | Assert.Equal(customerCount, InMemorySqliteTestDbContext.Customers.Count()); 76 | Assert.Equal(shoppingCartCount, InMemorySqliteTestDbContext.ShoppingCarts.Count()); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - DDD.NetCore.Web 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 41 |
42 | @RenderBody() 43 |
44 |
45 |

© 2017 - DDD.NetCore.Web

46 |
47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 61 | 67 | 68 | 69 | 70 | @RenderSection("Scripts", required: false) 71 | 72 | 73 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Login.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model LoginModel 3 | 4 | @{ 5 | ViewData["Title"] = "Log in"; 6 | } 7 | 8 |

@ViewData["Title"]

9 |
10 |
11 |
12 |
13 |

Use a local account to log in.

14 |
15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 |
28 | 32 |
33 |
34 |
35 | 36 |
37 |
38 |

39 | Forgot your password? 40 |

41 |

42 | Register as a new user 43 |

44 |
45 |
46 |
47 |
48 |
49 |
50 |

Use another service to log in.

51 |
52 | @{ 53 | if ((Model.ExternalLogins?.Count ?? 0) == 0) 54 | { 55 |
56 |

57 | There are no external authentication services configured. See this article 58 | for details on setting up this ASP.NET application to support logging in via external services. 59 |

60 |
61 | } 62 | else 63 | { 64 |
65 |
66 |

67 | @foreach (var provider in Model.ExternalLogins) 68 | { 69 | 70 | } 71 |

72 |
73 |
74 | } 75 | } 76 |
77 |
78 |
79 | 80 | @section Scripts { 81 | @await Html.PartialAsync("_ValidationScriptsPartial") 82 | } 83 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Register.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Text.Encodings.Web; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.Extensions.Logging; 10 | using DDD.NetCore.Web.Data; 11 | using DDD.NetCore.Web.Services; 12 | 13 | namespace DDD.NetCore.Web.Pages.Account 14 | { 15 | public class RegisterModel : PageModel 16 | { 17 | private readonly SignInManager _signInManager; 18 | private readonly UserManager _userManager; 19 | private readonly ILogger _logger; 20 | private readonly IEmailSender _emailSender; 21 | 22 | public RegisterModel( 23 | UserManager userManager, 24 | SignInManager signInManager, 25 | ILogger logger, 26 | IEmailSender emailSender) 27 | { 28 | _userManager = userManager; 29 | _signInManager = signInManager; 30 | _logger = logger; 31 | _emailSender = emailSender; 32 | } 33 | 34 | [BindProperty] 35 | public InputModel Input { get; set; } 36 | 37 | public string ReturnUrl { get; set; } 38 | 39 | public class InputModel 40 | { 41 | [Required] 42 | [EmailAddress] 43 | [Display(Name = "Email")] 44 | public string Email { get; set; } 45 | 46 | [Required] 47 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 48 | [DataType(DataType.Password)] 49 | [Display(Name = "Password")] 50 | public string Password { get; set; } 51 | 52 | [DataType(DataType.Password)] 53 | [Display(Name = "Confirm password")] 54 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 55 | public string ConfirmPassword { get; set; } 56 | } 57 | 58 | public void OnGet(string returnUrl = null) 59 | { 60 | ReturnUrl = returnUrl; 61 | } 62 | 63 | public async Task OnPostAsync(string returnUrl = null) 64 | { 65 | ReturnUrl = returnUrl; 66 | if (ModelState.IsValid) 67 | { 68 | var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; 69 | var result = await _userManager.CreateAsync(user, Input.Password); 70 | if (result.Succeeded) 71 | { 72 | _logger.LogInformation("User created a new account with password."); 73 | 74 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 75 | var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); 76 | await _emailSender.SendEmailConfirmationAsync(Input.Email, callbackUrl); 77 | 78 | await _signInManager.SignInAsync(user, isPersistent: false); 79 | return LocalRedirect(Url.GetLocalUrl(returnUrl)); 80 | } 81 | foreach (var error in result.Errors) 82 | { 83 | ModelState.AddModelError(string.Empty, error.Description); 84 | } 85 | } 86 | 87 | // If we got this far, something failed, redisplay form 88 | return Page(); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/LoginWith2fa.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.Extensions.Logging; 10 | using DDD.NetCore.Web.Data; 11 | 12 | namespace DDD.NetCore.Web.Pages.Account 13 | { 14 | public class LoginWith2faModel : PageModel 15 | { 16 | private readonly SignInManager _signInManager; 17 | private readonly ILogger _logger; 18 | 19 | public LoginWith2faModel(SignInManager signInManager, ILogger logger) 20 | { 21 | _signInManager = signInManager; 22 | _logger = logger; 23 | } 24 | 25 | [BindProperty] 26 | public InputModel Input { get; set; } 27 | 28 | public bool RememberMe { get; set; } 29 | 30 | public string ReturnUrl { get; set; } 31 | 32 | public class InputModel 33 | { 34 | [Required] 35 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 36 | [DataType(DataType.Text)] 37 | [Display(Name = "Authenticator code")] 38 | public string TwoFactorCode { get; set; } 39 | 40 | [Display(Name = "Remember this machine")] 41 | public bool RememberMachine { get; set; } 42 | } 43 | 44 | public async Task OnGetAsync(bool rememberMe, string returnUrl = null) 45 | { 46 | // Ensure the user has gone through the username & password screen first 47 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 48 | 49 | if (user == null) 50 | { 51 | throw new ApplicationException($"Unable to load two-factor authentication user."); 52 | } 53 | 54 | ReturnUrl = returnUrl; 55 | RememberMe = rememberMe; 56 | 57 | return Page(); 58 | } 59 | 60 | public async Task OnPostAsync(bool rememberMe, string returnUrl = null) 61 | { 62 | if (!ModelState.IsValid) 63 | { 64 | return Page(); 65 | } 66 | 67 | var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); 68 | if (user == null) 69 | { 70 | throw new ApplicationException($"Unable to load two-factor authentication user."); 71 | } 72 | 73 | var authenticatorCode = Input.TwoFactorCode.Replace(" ", string.Empty).Replace("-", string.Empty); 74 | 75 | var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(authenticatorCode, rememberMe, Input.RememberMachine); 76 | 77 | if (result.Succeeded) 78 | { 79 | _logger.LogInformation("User with ID '{UserId}' logged in with 2fa.", user.Id); 80 | return LocalRedirect(Url.GetLocalUrl(returnUrl)); 81 | } 82 | else if (result.IsLockedOut) 83 | { 84 | _logger.LogWarning("User with ID '{UserId}' account locked out.", user.Id); 85 | return RedirectToPage("./Lockout"); 86 | } 87 | else 88 | { 89 | _logger.LogWarning("Invalid authenticator code entered for user with ID '{UserId}'.", user.Id); 90 | ModelState.AddModelError(string.Empty, "Invalid authenticator code."); 91 | return Page(); 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Login.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | using Microsoft.Extensions.Logging; 11 | using DDD.NetCore.Web.Data; 12 | 13 | namespace DDD.NetCore.Web.Pages.Account 14 | { 15 | public class LoginModel : PageModel 16 | { 17 | private readonly SignInManager _signInManager; 18 | private readonly ILogger _logger; 19 | 20 | public LoginModel(SignInManager signInManager, ILogger logger) 21 | { 22 | _signInManager = signInManager; 23 | _logger = logger; 24 | } 25 | 26 | [BindProperty] 27 | public InputModel Input { get; set; } 28 | 29 | public IList ExternalLogins { get; set; } 30 | 31 | public string ReturnUrl { get; set; } 32 | 33 | [TempData] 34 | public string ErrorMessage { get; set; } 35 | 36 | public class InputModel 37 | { 38 | [Required] 39 | [EmailAddress] 40 | public string Email { get; set; } 41 | 42 | [Required] 43 | [DataType(DataType.Password)] 44 | public string Password { get; set; } 45 | 46 | [Display(Name = "Remember me?")] 47 | public bool RememberMe { get; set; } 48 | } 49 | 50 | public async Task OnGetAsync(string returnUrl = null) 51 | { 52 | if (!string.IsNullOrEmpty(ErrorMessage)) 53 | { 54 | ModelState.AddModelError(string.Empty, ErrorMessage); 55 | } 56 | 57 | // Clear the existing external cookie to ensure a clean login process 58 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 59 | 60 | ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); 61 | 62 | ReturnUrl = returnUrl; 63 | } 64 | 65 | public async Task OnPostAsync(string returnUrl = null) 66 | { 67 | ReturnUrl = returnUrl; 68 | 69 | if (ModelState.IsValid) 70 | { 71 | // This doesn't count login failures towards account lockout 72 | // To enable password failures to trigger account lockout, set lockoutOnFailure: true 73 | var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true); 74 | if (result.Succeeded) 75 | { 76 | _logger.LogInformation("User logged in."); 77 | return LocalRedirect(Url.GetLocalUrl(returnUrl)); 78 | } 79 | if (result.RequiresTwoFactor) 80 | { 81 | return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe }); 82 | } 83 | if (result.IsLockedOut) 84 | { 85 | _logger.LogWarning("User account locked out."); 86 | return RedirectToPage("./Lockout"); 87 | } 88 | else 89 | { 90 | ModelState.AddModelError(string.Empty, "Invalid login attempt."); 91 | return Page(); 92 | } 93 | } 94 | 95 | // If we got this far, something failed, redisplay form 96 | return Page(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Entities/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace DDD.NetCore.Domain.Entities 6 | { 7 | /// 8 | /// A shortcut of for most used primary key type (). 9 | /// 10 | public abstract class Entity : Entity, IEntity 11 | { 12 | 13 | } 14 | 15 | /// 16 | /// Basic implementation of IEntity interface. 17 | /// An entity can inherit this class of directly implement to IEntity interface. 18 | /// 19 | /// Type of the primary key of the entity 20 | public abstract class Entity : IEntity 21 | { 22 | /// 23 | /// Unique identifier for this entity. 24 | /// 25 | public virtual TPrimaryKey Id { get; set; } 26 | 27 | /// 28 | /// Checks if this entity is transient (it has not an Id). 29 | /// 30 | /// True, if this entity is transient 31 | public virtual bool IsTransient() 32 | { 33 | if (EqualityComparer.Default.Equals(Id, default(TPrimaryKey))) 34 | { 35 | return true; 36 | } 37 | 38 | //Workaround for EF Core since it sets int/long to min value when attaching to dbcontext 39 | if (typeof(TPrimaryKey) == typeof(int)) 40 | { 41 | return Convert.ToInt32(Id) <= 0; 42 | } 43 | 44 | if (typeof(TPrimaryKey) == typeof(long)) 45 | { 46 | return Convert.ToInt64(Id) <= 0; 47 | } 48 | 49 | return false; 50 | } 51 | 52 | /// 53 | public override bool Equals(object obj) 54 | { 55 | if (obj == null || !(obj is Entity)) 56 | { 57 | return false; 58 | } 59 | 60 | //Same instances must be considered as equal 61 | if (ReferenceEquals(this, obj)) 62 | { 63 | return true; 64 | } 65 | 66 | //Transient objects are not considered as equal 67 | var other = (Entity)obj; 68 | if (IsTransient() && other.IsTransient()) 69 | { 70 | return false; 71 | } 72 | 73 | //Must have a IS-A relation of types or must be same type 74 | var typeOfThis = GetType(); 75 | var typeOfOther = other.GetType(); 76 | if (!typeOfThis.GetTypeInfo().IsAssignableFrom(typeOfOther) && !typeOfOther.GetTypeInfo().IsAssignableFrom(typeOfThis)) 77 | { 78 | return false; 79 | } 80 | 81 | return Id.Equals(other.Id); 82 | } 83 | 84 | /// 85 | public override int GetHashCode() 86 | { 87 | return Id.GetHashCode(); 88 | } 89 | 90 | /// 91 | public static bool operator ==(Entity left, Entity right) 92 | { 93 | if (Equals(left, null)) 94 | { 95 | return Equals(right, null); 96 | } 97 | 98 | return left.Equals(right); 99 | } 100 | 101 | /// 102 | public static bool operator !=(Entity left, Entity right) 103 | { 104 | return !(left == right); 105 | } 106 | 107 | /// 108 | public override string ToString() 109 | { 110 | return $"[{GetType().Name} {Id}]"; 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ChangePassword.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using Microsoft.Extensions.Logging; 10 | using DDD.NetCore.Web.Data; 11 | 12 | namespace DDD.NetCore.Web.Pages.Account.Manage 13 | { 14 | public class ChangePasswordModel : PageModel 15 | { 16 | private readonly UserManager _userManager; 17 | private readonly SignInManager _signInManager; 18 | private readonly ILogger _logger; 19 | 20 | public ChangePasswordModel( 21 | UserManager userManager, 22 | SignInManager signInManager, 23 | ILogger logger) 24 | { 25 | _userManager = userManager; 26 | _signInManager = signInManager; 27 | _logger = logger; 28 | } 29 | 30 | [BindProperty] 31 | public InputModel Input { get; set; } 32 | 33 | [TempData] 34 | public string StatusMessage { get; set; } 35 | 36 | public class InputModel 37 | { 38 | [Required] 39 | [DataType(DataType.Password)] 40 | [Display(Name = "Current password")] 41 | public string OldPassword { get; set; } 42 | 43 | [Required] 44 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 45 | [DataType(DataType.Password)] 46 | [Display(Name = "New password")] 47 | public string NewPassword { get; set; } 48 | 49 | [DataType(DataType.Password)] 50 | [Display(Name = "Confirm new password")] 51 | [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] 52 | public string ConfirmPassword { get; set; } 53 | } 54 | 55 | public async Task OnGetAsync() 56 | { 57 | var user = await _userManager.GetUserAsync(User); 58 | if (user == null) 59 | { 60 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 61 | } 62 | 63 | var hasPassword = await _userManager.HasPasswordAsync(user); 64 | if (!hasPassword) 65 | { 66 | return RedirectToPage("./SetPassword"); 67 | } 68 | 69 | return Page(); 70 | } 71 | 72 | public async Task OnPostAsync() 73 | { 74 | if (!ModelState.IsValid) 75 | { 76 | return Page(); 77 | } 78 | 79 | var user = await _userManager.GetUserAsync(User); 80 | if (user == null) 81 | { 82 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 83 | } 84 | 85 | var changePasswordResult = await _userManager.ChangePasswordAsync(user, Input.OldPassword, Input.NewPassword); 86 | if (!changePasswordResult.Succeeded) 87 | { 88 | foreach (var error in changePasswordResult.Errors) 89 | { 90 | ModelState.AddModelError(string.Empty, error.Description); 91 | } 92 | return Page(); 93 | } 94 | 95 | await _signInManager.SignInAsync(user, isPersistent: false); 96 | _logger.LogInformation("User changed their password successfully."); 97 | StatusMessage = "Your password has been changed."; 98 | 99 | return RedirectToPage(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /DDD.NetCore.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26730.3 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.NetCore.Web", "DDD.NetCore.Web\DDD.NetCore.Web.csproj", "{8182E4C8-C906-4C72-92BC-720E11BBFE11}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.NetCore", "DDD.NetCore\DDD.NetCore.csproj", "{C387ED6D-25B5-4B57-BB7A-5CD88DDBD50E}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.NetCore.Domain", "DDD.NetCore.Domain\DDD.NetCore.Domain.csproj", "{00365BC2-A1CF-4CFF-8CAF-5E1E49EFD1A3}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.NetCore.Application", "DDD.NetCore.Application\DDD.NetCore.Application.csproj", "{794B50EE-EE39-4FB6-8291-8F40C366217C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DDD.NetCore.Infrastructure", "DDD.NetCore.Infrastructure\DDD.NetCore.Infrastructure.csproj", "{27F1B2E8-2AAF-436E-911A-54E966F75C10}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DDD.NetCore.Test", "DDD.NetCore.Test\DDD.NetCore.Test.csproj", "{D8CD6278-DF91-4245-8A08-519E3EA5A0B7}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{50877D5A-ECC8-466F-B1CA-855C3408EE87}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {8182E4C8-C906-4C72-92BC-720E11BBFE11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {8182E4C8-C906-4C72-92BC-720E11BBFE11}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {8182E4C8-C906-4C72-92BC-720E11BBFE11}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {8182E4C8-C906-4C72-92BC-720E11BBFE11}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {C387ED6D-25B5-4B57-BB7A-5CD88DDBD50E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C387ED6D-25B5-4B57-BB7A-5CD88DDBD50E}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C387ED6D-25B5-4B57-BB7A-5CD88DDBD50E}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C387ED6D-25B5-4B57-BB7A-5CD88DDBD50E}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {00365BC2-A1CF-4CFF-8CAF-5E1E49EFD1A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {00365BC2-A1CF-4CFF-8CAF-5E1E49EFD1A3}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {00365BC2-A1CF-4CFF-8CAF-5E1E49EFD1A3}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {00365BC2-A1CF-4CFF-8CAF-5E1E49EFD1A3}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {794B50EE-EE39-4FB6-8291-8F40C366217C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {794B50EE-EE39-4FB6-8291-8F40C366217C}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {794B50EE-EE39-4FB6-8291-8F40C366217C}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {794B50EE-EE39-4FB6-8291-8F40C366217C}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {27F1B2E8-2AAF-436E-911A-54E966F75C10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {27F1B2E8-2AAF-436E-911A-54E966F75C10}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {27F1B2E8-2AAF-436E-911A-54E966F75C10}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {27F1B2E8-2AAF-436E-911A-54E966F75C10}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {D8CD6278-DF91-4245-8A08-519E3EA5A0B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {D8CD6278-DF91-4245-8A08-519E3EA5A0B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {D8CD6278-DF91-4245-8A08-519E3EA5A0B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {D8CD6278-DF91-4245-8A08-519E3EA5A0B7}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {D8CD6278-DF91-4245-8A08-519E3EA5A0B7} = {50877D5A-ECC8-466F-B1CA-855C3408EE87} 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {A7E33949-32A5-414E-B82E-A590515AEFFE} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/Index.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Text.Encodings.Web; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | using DDD.NetCore.Web.Data; 11 | using DDD.NetCore.Web.Services; 12 | 13 | namespace DDD.NetCore.Web.Pages.Account.Manage 14 | { 15 | public partial class IndexModel : PageModel 16 | { 17 | private readonly UserManager _userManager; 18 | private readonly SignInManager _signInManager; 19 | private readonly IEmailSender _emailSender; 20 | 21 | public IndexModel( 22 | UserManager userManager, 23 | SignInManager signInManager, 24 | IEmailSender emailSender) 25 | { 26 | _userManager = userManager; 27 | _signInManager = signInManager; 28 | _emailSender = emailSender; 29 | } 30 | 31 | public string Username { get; set; } 32 | 33 | public bool IsEmailConfirmed { get; set; } 34 | 35 | [TempData] 36 | public string StatusMessage { get; set; } 37 | 38 | [BindProperty] 39 | public InputModel Input { get; set; } 40 | 41 | public class InputModel 42 | { 43 | [Required] 44 | [EmailAddress] 45 | public string Email { get; set; } 46 | 47 | [Phone] 48 | [Display(Name = "Phone number")] 49 | public string PhoneNumber { get; set; } 50 | } 51 | 52 | public async Task OnGetAsync() 53 | { 54 | var user = await _userManager.GetUserAsync(User); 55 | if (user == null) 56 | { 57 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 58 | } 59 | 60 | Username = user.UserName; 61 | 62 | Input = new InputModel 63 | { 64 | Email = user.Email, 65 | PhoneNumber = user.PhoneNumber 66 | }; 67 | 68 | IsEmailConfirmed = await _userManager.IsEmailConfirmedAsync(user); 69 | 70 | return Page(); 71 | } 72 | 73 | public async Task OnPostAsync() 74 | { 75 | if (!ModelState.IsValid) 76 | { 77 | return Page(); 78 | } 79 | 80 | var user = await _userManager.GetUserAsync(User); 81 | if (user == null) 82 | { 83 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 84 | } 85 | 86 | if (Input.Email != user.Email) 87 | { 88 | var setEmailResult = await _userManager.SetEmailAsync(user, Input.Email); 89 | if (!setEmailResult.Succeeded) 90 | { 91 | throw new ApplicationException($"Unexpected error occurred setting email for user with ID '{user.Id}'."); 92 | } 93 | } 94 | 95 | if (Input.PhoneNumber != user.PhoneNumber) 96 | { 97 | var setPhoneResult = await _userManager.SetPhoneNumberAsync(user, Input.PhoneNumber); 98 | if (!setPhoneResult.Succeeded) 99 | { 100 | throw new ApplicationException($"Unexpected error occurred setting phone number for user with ID '{user.Id}'."); 101 | } 102 | } 103 | 104 | StatusMessage = "Your profile has been updated"; 105 | return RedirectToPage(); 106 | } 107 | public async Task OnPostSendVerificationEmailAsync() 108 | { 109 | if (!ModelState.IsValid) 110 | { 111 | return Page(); 112 | } 113 | 114 | var user = await _userManager.GetUserAsync(User); 115 | if (user == null) 116 | { 117 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 118 | } 119 | 120 | var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); 121 | var callbackUrl = Url.EmailConfirmationLink(user.Id, code, Request.Scheme); 122 | await _emailSender.SendEmailConfirmationAsync(user.Email, callbackUrl); 123 | 124 | StatusMessage = "Verification email sent. Please check your email."; 125 | return RedirectToPage(); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/ExternalLogins.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Authentication; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.RazorPages; 9 | using DDD.NetCore.Web.Data; 10 | 11 | namespace DDD.NetCore.Web.Pages.Account.Manage 12 | { 13 | public class ExternalLoginsModel : PageModel 14 | { 15 | private readonly UserManager _userManager; 16 | private readonly SignInManager _signInManager; 17 | 18 | public ExternalLoginsModel( 19 | UserManager userManager, 20 | SignInManager signInManager) 21 | { 22 | _userManager = userManager; 23 | _signInManager = signInManager; 24 | } 25 | 26 | public IList CurrentLogins { get; set; } 27 | 28 | public IList OtherLogins { get; set; } 29 | 30 | public bool ShowRemoveButton { get; set; } 31 | 32 | [TempData] 33 | public string StatusMessage { get; set; } 34 | 35 | public async Task OnGetAsync() 36 | { 37 | var user = await _userManager.GetUserAsync(User); 38 | if (user == null) 39 | { 40 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 41 | } 42 | 43 | CurrentLogins = await _userManager.GetLoginsAsync(user); 44 | OtherLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()) 45 | .Where(auth => CurrentLogins.All(ul => auth.Name != ul.LoginProvider)) 46 | .ToList(); 47 | ShowRemoveButton = user.PasswordHash != null || CurrentLogins.Count > 1; 48 | return Page(); 49 | } 50 | 51 | public async Task OnPostRemoveLoginAsync(string loginProvider, string providerKey) 52 | { 53 | var user = await _userManager.GetUserAsync(User); 54 | if (user == null) 55 | { 56 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 57 | } 58 | 59 | var result = await _userManager.RemoveLoginAsync(user, loginProvider, providerKey); 60 | if (!result.Succeeded) 61 | { 62 | throw new ApplicationException($"Unexpected error occurred removing external login for user with ID '{user.Id}'."); 63 | } 64 | 65 | await _signInManager.SignInAsync(user, isPersistent: false); 66 | StatusMessage = "The external login was removed."; 67 | return RedirectToPage(); 68 | } 69 | 70 | public async Task OnPostLinkLoginAsync(string provider) 71 | { 72 | // Clear the existing external cookie to ensure a clean login process 73 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 74 | 75 | // Request a redirect to the external login provider to link a login for the current user 76 | var redirectUrl = Url.Page("./ExternalLogins", pageHandler: "LinkLoginCallback"); 77 | var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); 78 | return new ChallengeResult(provider, properties); 79 | } 80 | 81 | public async Task OnGetLinkLoginCallbackAsync() 82 | { 83 | var user = await _userManager.GetUserAsync(User); 84 | if (user == null) 85 | { 86 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 87 | } 88 | 89 | var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); 90 | if (info == null) 91 | { 92 | throw new ApplicationException($"Unexpected error occurred loading external login info for user with ID '{user.Id}'."); 93 | } 94 | 95 | var result = await _userManager.AddLoginAsync(user, info); 96 | if (!result.Succeeded) 97 | { 98 | throw new ApplicationException($"Unexpected error occurred adding external login for user with ID '{user.Id}'."); 99 | } 100 | 101 | // Clear the existing external cookie to ensure a clean login process 102 | await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme); 103 | 104 | StatusMessage = "The external login was added."; 105 | return RedirectToPage(); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** Unobtrusive validation support library for jQuery and jQuery Validate 3 | ** Copyright (C) Microsoft Corporation. All rights reserved. 4 | */ 5 | !function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function m(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=p.unobtrusive.options||{},m=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),m("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),m("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),m("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var u,p=a.validator,v="unobtrusiveValidation";p.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=m(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){p.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=m(this);a&&a.attachValidation()})}},u=p.unobtrusive.adapters,u.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},u.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},u.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},u.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},p.addMethod("__dummy__",function(a,e,n){return!0}),p.addMethod("regex",function(a,e,n){var t;return this.optional(e)?!0:(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),p.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),p.methods.extension?(u.addSingleVal("accept","mimtype"),u.addSingleVal("extension","extension")):u.addSingleVal("extension","extension","accept"),u.addSingleVal("regex","pattern"),u.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),u.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),u.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),u.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),u.add("required",function(a){("INPUT"!==a.element.tagName.toUpperCase()||"CHECKBOX"!==a.element.type.toUpperCase())&&e(a,"required",!0)}),u.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),u.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),a(function(){p.unobtrusive.parse(document)})}(jQuery); -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/Manage/EnableAuthenticator.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | using System.Text.Encodings.Web; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using Microsoft.AspNetCore.Identity; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.AspNetCore.Mvc.RazorPages; 12 | using Microsoft.Extensions.Logging; 13 | using DDD.NetCore.Web.Data; 14 | 15 | namespace DDD.NetCore.Web.Pages.Account.Manage 16 | { 17 | public class EnableAuthenticatorModel : PageModel 18 | { 19 | private readonly UserManager _userManager; 20 | private readonly ILogger _logger; 21 | private readonly UrlEncoder _urlEncoder; 22 | 23 | private const string AuthenicatorUriFormat = "otpauth://totp/{0}:{1}?secret={2}&issuer={0}&digits=6"; 24 | 25 | public EnableAuthenticatorModel( 26 | UserManager userManager, 27 | ILogger logger, 28 | UrlEncoder urlEncoder) 29 | { 30 | _userManager = userManager; 31 | _logger = logger; 32 | _urlEncoder = urlEncoder; 33 | } 34 | 35 | public string SharedKey { get; set; } 36 | 37 | public string AuthenticatorUri { get; set; } 38 | 39 | [BindProperty] 40 | public InputModel Input { get; set; } 41 | 42 | public class InputModel 43 | { 44 | [Required] 45 | [StringLength(7, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 46 | [DataType(DataType.Text)] 47 | [Display(Name = "Verification Code")] 48 | public string Code { get; set; } 49 | } 50 | 51 | public async Task OnGetAsync() 52 | { 53 | var user = await _userManager.GetUserAsync(User); 54 | if (user == null) 55 | { 56 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 57 | } 58 | 59 | await LoadSharedKeyAndQrCodeUriAsync(user); 60 | if (string.IsNullOrEmpty(SharedKey)) 61 | { 62 | await _userManager.ResetAuthenticatorKeyAsync(user); 63 | await LoadSharedKeyAndQrCodeUriAsync(user); 64 | } 65 | 66 | return Page(); 67 | } 68 | 69 | public async Task OnPostAsync() 70 | { 71 | var user = await _userManager.GetUserAsync(User); 72 | if (user == null) 73 | { 74 | throw new ApplicationException($"Unable to load user with ID '{_userManager.GetUserId(User)}'."); 75 | } 76 | 77 | if (!ModelState.IsValid) 78 | { 79 | await LoadSharedKeyAndQrCodeUriAsync(user); 80 | return Page(); 81 | } 82 | 83 | // Strip spaces and hypens 84 | var verificationCode = Input.Code.Replace(" ", string.Empty).Replace("-", string.Empty); 85 | 86 | var is2faTokenValid = await _userManager.VerifyTwoFactorTokenAsync( 87 | user, _userManager.Options.Tokens.AuthenticatorTokenProvider, verificationCode); 88 | 89 | if (!is2faTokenValid) 90 | { 91 | ModelState.AddModelError("Input.Code", "Verification code is invalid."); 92 | await LoadSharedKeyAndQrCodeUriAsync(user); 93 | return Page(); 94 | } 95 | 96 | await _userManager.SetTwoFactorEnabledAsync(user, true); 97 | _logger.LogInformation("User with ID '{UserId}' has enabled 2FA with an authenticator app.", user.Id); 98 | return RedirectToPage("./GenerateRecoveryCodes"); 99 | } 100 | 101 | private async Task LoadSharedKeyAndQrCodeUriAsync(ApplicationUser user) 102 | { 103 | // Load the authenticator key & QR code URI to display on the form 104 | var unformattedKey = await _userManager.GetAuthenticatorKeyAsync(user); 105 | if (!string.IsNullOrEmpty(unformattedKey)) 106 | { 107 | SharedKey = FormatKey(unformattedKey); 108 | AuthenticatorUri = GenerateQrCodeUri(user.Email, unformattedKey); 109 | } 110 | } 111 | 112 | private string FormatKey(string unformattedKey) 113 | { 114 | var result = new StringBuilder(); 115 | int currentPosition = 0; 116 | while (currentPosition + 4 < unformattedKey.Length) 117 | { 118 | result.Append(unformattedKey.Substring(currentPosition, 4)).Append(" "); 119 | currentPosition += 4; 120 | } 121 | if (currentPosition < unformattedKey.Length) 122 | { 123 | result.Append(unformattedKey.Substring(currentPosition)); 124 | } 125 | 126 | return result.ToString().ToLowerInvariant(); 127 | } 128 | 129 | private string GenerateQrCodeUri(string email, string unformattedKey) 130 | { 131 | return string.Format( 132 | AuthenicatorUriFormat, 133 | _urlEncoder.Encode("DDD.NetCore.Web"), 134 | _urlEncoder.Encode(email), 135 | unformattedKey); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Account/ExternalLogin.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Security.Claims; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Identity; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.RazorPages; 10 | using Microsoft.Extensions.Logging; 11 | using DDD.NetCore.Web.Data; 12 | 13 | namespace DDD.NetCore.Web.Pages.Account 14 | { 15 | public class ExternalLoginModel : PageModel 16 | { 17 | private readonly SignInManager _signInManager; 18 | private readonly UserManager _userManager; 19 | private readonly ILogger _logger; 20 | 21 | public ExternalLoginModel( 22 | SignInManager signInManager, 23 | UserManager userManager, 24 | ILogger logger) 25 | { 26 | _signInManager = signInManager; 27 | _userManager = userManager; 28 | _logger = logger; 29 | } 30 | 31 | [BindProperty] 32 | public InputModel Input { get; set; } 33 | 34 | public string LoginProvider { get; set; } 35 | 36 | public string ReturnUrl { get; set; } 37 | 38 | [TempData] 39 | public string ErrorMessage { get; set; } 40 | 41 | public class InputModel 42 | { 43 | [Required] 44 | [EmailAddress] 45 | public string Email { get; set; } 46 | } 47 | 48 | public IActionResult OnGetAsync() 49 | { 50 | return RedirectToPage("./Login"); 51 | } 52 | 53 | public IActionResult OnPost(string provider, string returnUrl = null) 54 | { 55 | // Request a redirect to the external login provider. 56 | var redirectUrl = Url.Page("./ExternalLogin", pageHandler: "Callback", values: new { returnUrl }); 57 | var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); 58 | return new ChallengeResult(provider, properties); 59 | } 60 | 61 | public async Task OnGetCallbackAsync(string returnUrl = null, string remoteError = null) 62 | { 63 | if (remoteError != null) 64 | { 65 | ErrorMessage = $"Error from external provider: {remoteError}"; 66 | return RedirectToPage("./Login"); 67 | } 68 | var info = await _signInManager.GetExternalLoginInfoAsync(); 69 | if (info == null) 70 | { 71 | return RedirectToPage("./Login"); 72 | } 73 | 74 | // Sign in the user with this external login provider if the user already has a login. 75 | var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor : true); 76 | if (result.Succeeded) 77 | { 78 | _logger.LogInformation("{Name} logged in with {LoginProvider} provider.", info.Principal.Identity.Name, info.LoginProvider); 79 | return LocalRedirect(Url.GetLocalUrl(returnUrl)); 80 | } 81 | if (result.IsLockedOut) 82 | { 83 | return RedirectToPage("./Lockout"); 84 | } 85 | else 86 | { 87 | // If the user does not have an account, then ask the user to create an account. 88 | ReturnUrl = returnUrl; 89 | LoginProvider = info.LoginProvider; 90 | if (info.Principal.HasClaim(c => c.Type == ClaimTypes.Email)) 91 | { 92 | Input = new InputModel 93 | { 94 | Email = info.Principal.FindFirstValue(ClaimTypes.Email) 95 | }; 96 | } 97 | return Page(); 98 | } 99 | } 100 | 101 | public async Task OnPostConfirmationAsync(string returnUrl = null) 102 | { 103 | if (ModelState.IsValid) 104 | { 105 | // Get the information about the user from the external login provider 106 | var info = await _signInManager.GetExternalLoginInfoAsync(); 107 | if (info == null) 108 | { 109 | throw new ApplicationException("Error loading external login information during confirmation."); 110 | } 111 | var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email }; 112 | var result = await _userManager.CreateAsync(user); 113 | if (result.Succeeded) 114 | { 115 | result = await _userManager.AddLoginAsync(user, info); 116 | if (result.Succeeded) 117 | { 118 | await _signInManager.SignInAsync(user, isPersistent: false); 119 | _logger.LogInformation("User created an account using {Name} provider.", info.LoginProvider); 120 | return LocalRedirect(Url.GetLocalUrl(returnUrl)); 121 | } 122 | } 123 | foreach (var error in result.Errors) 124 | { 125 | ModelState.AddModelError(string.Empty, error.Description); 126 | } 127 | } 128 | 129 | ReturnUrl = returnUrl; 130 | return Page(); 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /DDD.NetCore.Web/Pages/Index.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model IndexModel 3 | @{ 4 | ViewData["Title"] = "Home page"; 5 | } 6 | 7 | 69 | 70 |
    71 |
    72 |

    Application uses

    73 |
      74 |
    • Sample pages using ASP.NET Core Razor Pages
    • 75 |
    • Bower for managing client-side libraries
    • 76 |
    • Theming using Bootstrap
    • 77 |
    78 |
    79 | 90 | 102 | 110 |
    111 | -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Repositories/EfCoreRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | using DDD.NetCore.Domain.Entities; 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace DDD.NetCore.Domain.Repositories 10 | { 11 | 12 | public class EfCoreRepository : EfCoreRepository 13 | where TEntity : class, IEntity 14 | where TDbContext : DbContext 15 | { 16 | public EfCoreRepository(TDbContext dbContext) : base(dbContext) 17 | { 18 | } 19 | } 20 | 21 | /// 22 | /// Implements IRepository for Entity Framework. 23 | /// 24 | /// DbContext which contains . 25 | /// Type of the Entity for this repository 26 | /// Primary key of the entity 27 | public class EfCoreRepository : RepositoryBase 28 | where TEntity : class, IEntity 29 | where TDbContext : DbContext 30 | { 31 | 32 | /// 33 | /// Gets DbSet for given entity. 34 | /// 35 | public virtual DbSet Table => _dbContext.Set(); 36 | 37 | private readonly TDbContext _dbContext; 38 | 39 | public EfCoreRepository(TDbContext dbContext) 40 | { 41 | _dbContext = dbContext; 42 | } 43 | 44 | public override IQueryable GetAll() 45 | { 46 | return Table; 47 | } 48 | 49 | public override async Task> GetAllListAsync() 50 | { 51 | return await GetAll().ToListAsync(); 52 | } 53 | 54 | public override async Task> GetAllListAsync(Expression> predicate) 55 | { 56 | return await GetAll().Where(predicate).ToListAsync(); 57 | } 58 | 59 | public override async Task FirstOrDefaultAsync(TPrimaryKey id) 60 | { 61 | return await GetAll().FirstOrDefaultAsync(CreateEqualityExpressionForId(id)); 62 | } 63 | 64 | public override async Task FirstOrDefaultAsync(Expression> predicate) 65 | { 66 | return await GetAll().FirstOrDefaultAsync(predicate); 67 | } 68 | 69 | public override TEntity Insert(TEntity entity) 70 | { 71 | var result = Table.Add(entity); 72 | return result.Entity; 73 | 74 | } 75 | 76 | public override Task InsertAsync(TEntity entity) 77 | { 78 | return Task.FromResult(Insert(entity)); 79 | } 80 | 81 | public override TPrimaryKey InsertAndGetId(TEntity entity) 82 | { 83 | entity = Insert(entity); 84 | 85 | if (entity.IsTransient()) 86 | { 87 | _dbContext.SaveChanges(); 88 | } 89 | 90 | return entity.Id; 91 | } 92 | 93 | public override async Task InsertAndGetIdAsync(TEntity entity) 94 | { 95 | entity = await InsertAsync(entity); 96 | 97 | if (entity.IsTransient()) 98 | { 99 | await _dbContext.SaveChangesAsync(); 100 | } 101 | 102 | return entity.Id; 103 | } 104 | 105 | public override TPrimaryKey InsertOrUpdateAndGetId(TEntity entity) 106 | { 107 | entity = InsertOrUpdate(entity); 108 | 109 | if (entity.IsTransient()) 110 | { 111 | _dbContext.SaveChanges(); 112 | } 113 | 114 | return entity.Id; 115 | } 116 | 117 | public override async Task InsertOrUpdateAndGetIdAsync(TEntity entity) 118 | { 119 | entity = await InsertOrUpdateAsync(entity); 120 | 121 | if (entity.IsTransient()) 122 | { 123 | await _dbContext.SaveChangesAsync(); 124 | } 125 | 126 | return entity.Id; 127 | } 128 | 129 | public override TEntity Update(TEntity entity) 130 | { 131 | AttachIfNot(entity); 132 | _dbContext.Entry(entity).State = EntityState.Modified; 133 | return entity; 134 | } 135 | 136 | public override Task UpdateAsync(TEntity entity) 137 | { 138 | AttachIfNot(entity); 139 | _dbContext.Entry(entity).State = EntityState.Modified; 140 | return Task.FromResult(entity); 141 | } 142 | 143 | public override void Delete(TEntity entity) 144 | { 145 | AttachIfNot(entity); 146 | Table.Remove(entity); 147 | } 148 | 149 | public override void Delete(TPrimaryKey id) 150 | { 151 | var entity = Table.Local.FirstOrDefault(ent => EqualityComparer.Default.Equals(ent.Id, id)); 152 | if (entity == null) 153 | { 154 | entity = FirstOrDefault(id); 155 | if (entity == null) 156 | { 157 | return; 158 | } 159 | } 160 | 161 | Delete(entity); 162 | } 163 | 164 | public override async Task CountAsync() 165 | { 166 | return await GetAll().CountAsync(); 167 | } 168 | 169 | public override async Task CountAsync(Expression> predicate) 170 | { 171 | return await GetAll().Where(predicate).CountAsync(); 172 | } 173 | 174 | public override async Task LongCountAsync() 175 | { 176 | return await GetAll().LongCountAsync(); 177 | } 178 | 179 | public override async Task LongCountAsync(Expression> predicate) 180 | { 181 | return await GetAll().Where(predicate).LongCountAsync(); 182 | } 183 | 184 | protected virtual void AttachIfNot(TEntity entity) 185 | { 186 | if (!Table.Local.Contains(entity)) 187 | { 188 | Table.Attach(entity); 189 | } 190 | } 191 | 192 | public DbContext GetDbContext() 193 | { 194 | return _dbContext; 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /DDD.NetCore/Domain/Repositories/RepositoryBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | using DDD.NetCore.Domain.Entities; 7 | using DDD.NetCore.Exception; 8 | 9 | namespace DDD.NetCore.Domain.Repositories 10 | { 11 | /// 12 | /// Base class to implement . 13 | /// It implements some methods in most simple way. 14 | /// 15 | /// Type of the Entity for this repository 16 | /// Primary key of the entity 17 | public abstract class RepositoryBase : IRepository 18 | where TEntity : class, IEntity 19 | { 20 | public abstract IQueryable GetAll(); 21 | 22 | public virtual IQueryable GetAllIncluding(params Expression>[] propertySelectors) 23 | { 24 | return GetAll(); 25 | } 26 | 27 | public virtual List GetAllList() 28 | { 29 | return GetAll().ToList(); 30 | } 31 | 32 | public virtual Task> GetAllListAsync() 33 | { 34 | return Task.FromResult(GetAllList()); 35 | } 36 | 37 | public virtual List GetAllList(Expression> predicate) 38 | { 39 | return GetAll().Where(predicate).ToList(); 40 | } 41 | 42 | public virtual Task> GetAllListAsync(Expression> predicate) 43 | { 44 | return Task.FromResult(GetAllList(predicate)); 45 | } 46 | 47 | public virtual T Query(Func, T> queryMethod) 48 | { 49 | return queryMethod(GetAll()); 50 | } 51 | 52 | public virtual TEntity Get(TPrimaryKey id) 53 | { 54 | var entity = FirstOrDefault(id); 55 | if (entity == null) 56 | { 57 | throw new EntityNotFoundException(typeof(TEntity), id); 58 | } 59 | 60 | return entity; 61 | } 62 | 63 | public virtual async Task GetAsync(TPrimaryKey id) 64 | { 65 | var entity = await FirstOrDefaultAsync(id); 66 | if (entity == null) 67 | { 68 | throw new EntityNotFoundException(typeof(TEntity), id); 69 | } 70 | 71 | return entity; 72 | } 73 | 74 | public virtual TEntity FirstOrDefault(TPrimaryKey id) 75 | { 76 | return GetAll().FirstOrDefault(CreateEqualityExpressionForId(id)); 77 | } 78 | 79 | public virtual Task FirstOrDefaultAsync(TPrimaryKey id) 80 | { 81 | return Task.FromResult(FirstOrDefault(id)); 82 | } 83 | 84 | public virtual TEntity FirstOrDefault(Expression> predicate) 85 | { 86 | return GetAll().FirstOrDefault(predicate); 87 | } 88 | 89 | public virtual Task FirstOrDefaultAsync(Expression> predicate) 90 | { 91 | return Task.FromResult(FirstOrDefault(predicate)); 92 | } 93 | 94 | public abstract TEntity Insert(TEntity entity); 95 | 96 | public virtual Task InsertAsync(TEntity entity) 97 | { 98 | return Task.FromResult(Insert(entity)); 99 | } 100 | 101 | public virtual TPrimaryKey InsertAndGetId(TEntity entity) 102 | { 103 | return Insert(entity).Id; 104 | } 105 | 106 | public virtual Task InsertAndGetIdAsync(TEntity entity) 107 | { 108 | return Task.FromResult(InsertAndGetId(entity)); 109 | } 110 | 111 | public virtual TEntity InsertOrUpdate(TEntity entity) 112 | { 113 | return entity.IsTransient() 114 | ? Insert(entity) 115 | : Update(entity); 116 | } 117 | 118 | public virtual async Task InsertOrUpdateAsync(TEntity entity) 119 | { 120 | return entity.IsTransient() 121 | ? await InsertAsync(entity) 122 | : await UpdateAsync(entity); 123 | } 124 | 125 | public virtual TPrimaryKey InsertOrUpdateAndGetId(TEntity entity) 126 | { 127 | return InsertOrUpdate(entity).Id; 128 | } 129 | 130 | public virtual Task InsertOrUpdateAndGetIdAsync(TEntity entity) 131 | { 132 | return Task.FromResult(InsertOrUpdateAndGetId(entity)); 133 | } 134 | 135 | public abstract TEntity Update(TEntity entity); 136 | 137 | public virtual Task UpdateAsync(TEntity entity) 138 | { 139 | return Task.FromResult(Update(entity)); 140 | } 141 | 142 | public abstract void Delete(TEntity entity); 143 | 144 | public virtual Task DeleteAsync(TEntity entity) 145 | { 146 | Delete(entity); 147 | return Task.FromResult(0); 148 | } 149 | 150 | public abstract void Delete(TPrimaryKey id); 151 | 152 | public virtual Task DeleteAsync(TPrimaryKey id) 153 | { 154 | Delete(id); 155 | return Task.FromResult(0); 156 | } 157 | 158 | public virtual void Delete(Expression> predicate) 159 | { 160 | foreach (var entity in GetAll().Where(predicate).ToList()) 161 | { 162 | Delete(entity); 163 | } 164 | } 165 | 166 | public virtual Task DeleteAsync(Expression> predicate) 167 | { 168 | Delete(predicate); 169 | return Task.FromResult(0); 170 | } 171 | 172 | public virtual int Count() 173 | { 174 | return GetAll().Count(); 175 | } 176 | 177 | public virtual Task CountAsync() 178 | { 179 | return Task.FromResult(Count()); 180 | } 181 | 182 | public virtual int Count(Expression> predicate) 183 | { 184 | return GetAll().Where(predicate).Count(); 185 | } 186 | 187 | public virtual Task CountAsync(Expression> predicate) 188 | { 189 | return Task.FromResult(Count(predicate)); 190 | } 191 | 192 | public virtual long LongCount() 193 | { 194 | return GetAll().LongCount(); 195 | } 196 | 197 | public virtual Task LongCountAsync() 198 | { 199 | return Task.FromResult(LongCount()); 200 | } 201 | 202 | public virtual long LongCount(Expression> predicate) 203 | { 204 | return GetAll().Where(predicate).LongCount(); 205 | } 206 | 207 | public virtual Task LongCountAsync(Expression> predicate) 208 | { 209 | return Task.FromResult(LongCount(predicate)); 210 | } 211 | 212 | protected static Expression> CreateEqualityExpressionForId(TPrimaryKey id) 213 | { 214 | var lambdaParam = Expression.Parameter(typeof(TEntity)); 215 | 216 | var lambdaBody = Expression.Equal( 217 | Expression.PropertyOrField(lambdaParam, "Id"), 218 | Expression.Constant(id, typeof(TPrimaryKey)) 219 | ); 220 | 221 | return Expression.Lambda>(lambdaBody, lambdaParam); 222 | } 223 | } 224 | } -------------------------------------------------------------------------------- /DDD.NetCore.Web/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------