├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ └── dotnet-core.yml ├── .gitignore ├── CHANGELOG.md ├── Common ├── ApiResultStatusCode.cs ├── Common.csproj ├── Exceptions │ ├── AppException.cs │ ├── BadRequestException.cs │ ├── LogicException.cs │ └── NotFoundException.cs ├── IScopedDependency.cs ├── SiteSettings.cs └── Utilities │ ├── Assert.cs │ ├── EnumExcentions.cs │ ├── IdentityExtensions.cs │ ├── ModelBuilderExtensions.cs │ ├── ReflectionExtensions.cs │ ├── SecurityHelper.cs │ └── StringExtensions.cs ├── Data ├── ApplicationDbContext.cs ├── Contracts │ ├── IRepository.cs │ └── IUserRepository.cs ├── Data.csproj ├── Migrations │ ├── 20190209131717_initial database.Designer.cs │ ├── 20190209131717_initial database.cs │ └── ApplicationDbContextModelSnapshot.cs └── Repositories │ ├── Repository.cs │ └── UserRepository.cs ├── ELMAH-SQLServer.sql ├── Entities ├── Common │ └── BaseEntity.cs ├── Entities.csproj ├── Post │ ├── Category.cs │ └── Post.cs └── User │ ├── Role.cs │ └── User.cs ├── MyApi.sln ├── MyApi ├── Controllers │ ├── v1 │ │ ├── CategoriesController.cs │ │ ├── OldPostsController.cs │ │ ├── PostsController.cs │ │ ├── TestController.cs │ │ └── UsersController.cs │ └── v2 │ │ ├── PostsController.cs │ │ └── UsersController.cs ├── Models │ ├── CategoryDto.cs │ ├── PostCustomMapping.cs │ ├── PostDto.cs │ ├── TokenRequest.cs │ └── UserDto.cs ├── MyApi.csproj ├── MyApi.csproj.user ├── MyApi.xml ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── appsettings.Development.json ├── appsettings.json └── nlog.config ├── README.md ├── Services ├── AccessToken.cs ├── DataInitializer │ ├── CategoryDataInitializer.cs │ ├── IDataInitializer.cs │ └── UserDataInitializer.cs ├── Services.csproj └── Services │ ├── IJwtService.cs │ └── JwtService.cs └── WebFramework ├── Api ├── ApiResult.cs ├── BaseController.cs ├── BaseDto.cs └── CrudController.cs ├── Configuration ├── ApplicationBuilderExtensions.cs ├── AutofacConfigurationExtensions.cs ├── IdentityConfigurationExtensions.cs └── ServiceCollectionExtensions.cs ├── CustomMapping ├── AutoMapperConfiguration.cs ├── CustomMappingProfile.cs └── IHaveCustomMapping.cs ├── Filters └── ApiResultFilterAttribute.cs ├── Middlewares └── CustomExceptionHandlerMiddleware.cs ├── Swagger ├── ApplySummariesOperationFilter.cs ├── RemoveVersionParameters.cs ├── SetVersionInPaths.cs ├── SwaggerConfigurationExtensions.cs └── UnauthorizedResponsesOperationFilter.cs └── WebFramework.csproj /.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 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://coffeebede.ir/mjebrahimi'] 13 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'README.md' 7 | pull_request: 8 | paths-ignore: 9 | - 'README.md' 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2.4.0 19 | 20 | - name: Setup .NET Core 21 | uses: actions/setup-dotnet@v1.9.0 22 | with: 23 | dotnet-version: '6.0.x' 24 | 25 | - name: Install Dependencies 26 | run: dotnet restore 27 | 28 | - name: Build (Release) 29 | run: dotnet build --configuration Release --no-restore 30 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for Upgrade from ASP.NET Core 2.1 to ASP.NET Core 3.1 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [Update to AspNetCore v3.1.2] (2020-03-05) 5 | 6 | ### Summary 7 | - All packages **updated to the latest version**. 8 | - **Code improvement** and **bug fixes**. 9 | - **AutoMapper** v9.0.0 changes applied and static `Mapper` removed. 10 | - **Swashbuckle** new changes applied. 11 | - **NETCore 3.x** and **ASPNETCore 3.x** and **EFCore 3.x** new changes applied. 12 | 13 | ### Details 14 | 15 | - Because of updates in `AutoMapper` api and removing the static `Mapper` class, `IMapper` passed to all controller and services that uses mapping. (for example [OldPostsController.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-eaab1d7550c3c321acce8aed23408f31)) 16 | 17 | - Mapping implementation changed in [BaseDto.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-e7ce6de2fd70a2e3cffd18f3ada9e392) 18 | 19 | - Mapping configuration changed in [AutoMapperConfiguration.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-9eac783364651f0bb7476af1a68abc6c) 20 | 21 | - Package `Microsoft.EntityFrameworkCore.Tools` added. We need it since EFCore v3.x to use migration command in Nuget Package Manager Console. 22 | 23 | - Because of EFCore3.x api changes, `entityType.Relational().TableName` changed to `entityType.GetTableName()` and `property.Relational().DefaultValueSql` changed to `property.SetDefaultValueSql()` in [ModelBuilderExtensions.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-becd12d58ff3b004786ad98ed04ab5e9) 24 | 25 | - `ConfigureWarnings` removed in [ServiceCollectionExtensions.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-92fe43494731aa038324b5b4b89b795cL37) because automatic client evaluation is no longer supported and this event is no longer generated. 26 | 27 | - `ValueTask` replaced by `Task` in `GetByIdAsync` method of [Repository](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-94b60a9279541f7ec2317bd1ccb39e10R28) 28 | 29 | - Package `Swashbuckle.AspNetCore.Examples` replaced by `Swashbuckle.AspNetCore.Filters` and so their namespaces. 30 | 31 | - `IExamplesProvider` changed to generic version `IExamplesProvider` (see [CreateUserResponseExample](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/compare/AspNetCore2.1...master#diff-df20c65f37a9123d4f975cbd679a0b58L155)) 32 | 33 | - Package `NLog.Targets.Sentry2` replaced by `Sentry.NLog` and their related code changed in [Program.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-c47d99eda2de424251000d1108618003) and its [nlog.config](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-20fb3fba6421f6afb2c8c7e29ba38ef9) changed a little. 34 | 35 | - Due to model changes in OpenApi(Swagger), these files changed [ApplySummariesOperationFilter.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-4bec3d908f7d545fcef8aba09fcff33f), [RemoveVersionParameters.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-2cb5e55c2bd5965637f6157197bbcca8), [SetVersionInPaths.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-1cc6c72fa90caa6d8f9d0f68b5426ca1), [UnauthorizedResponsesOperationFilter.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-6ff134c9756202d7c244ca64c4334d75) 36 | 37 | - Swagger configuration in [SwaggerConfigurationExtensions.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-04b7fe07540f95c0feef71762d4427e2) changed due to swagger new updates 38 | 39 | - Implementation of [ApiResultFilterAttribute.cs](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-6666eb9214b52adc50e21618657a5fbe) changed due to ASPNETCore 3.x new updates 40 | 41 | - Implementation of `AddMinimalMvc` in [ServiceCollectionExtensions.cs ](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-92fe43494731aa038324b5b4b89b795cL41) method changed since `service.AddControllers()` was introduced. 42 | 43 | - `IHostingEnvironment` replaced by `IWebHostEnvironment` because of API deprecated in NETCore 3.x ([here](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-9630cb1f7e42938b535ac527b562c5b5) for example) 44 | 45 | - The `app.UseMvc()` replaced by `app.UseRouting()` and `app.UseEndpoints()` in [Startup.Configure](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/commit/512375f4768b4ed08f0e03cf58e14ac392cd1c3d#diff-a045957754a1b17bd3f0ba5e21bd0c13R68) method 46 | 47 | - Unused namespaces in all projects removed. 48 | 49 | [Update to AspNetCore v3.1.2]: https://github.com/dotnetzoom/AspNetCore-WebApi-Course/compare/AspNetCore2.1...master 50 | -------------------------------------------------------------------------------- /Common/ApiResultStatusCode.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Common 4 | { 5 | public enum ApiResultStatusCode 6 | { 7 | [Display(Name = "عملیات با موفقیت انجام شد")] 8 | Success = 0, 9 | 10 | [Display(Name = "خطایی در سرور رخ داده است")] 11 | ServerError = 1, 12 | 13 | [Display(Name = "پارامتر های ارسالی معتبر نیستند")] 14 | BadRequest = 2, 15 | 16 | [Display(Name = "یافت نشد")] 17 | NotFound = 3, 18 | 19 | [Display(Name = "لیست خالی است")] 20 | ListEmpty = 4, 21 | 22 | [Display(Name = "خطایی در پردازش رخ داد")] 23 | LogicError = 5, 24 | 25 | [Display(Name = "خطای احراز هویت")] 26 | UnAuthorized = 6 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Common/Common.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /Common/Exceptions/AppException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | 4 | namespace Common.Exceptions 5 | { 6 | public class AppException : Exception 7 | { 8 | public HttpStatusCode HttpStatusCode { get; set; } 9 | public ApiResultStatusCode ApiStatusCode { get; set; } 10 | public object AdditionalData { get; set; } 11 | 12 | public AppException() 13 | : this(ApiResultStatusCode.ServerError) 14 | { 15 | } 16 | 17 | public AppException(ApiResultStatusCode statusCode) 18 | : this(statusCode, null) 19 | { 20 | } 21 | 22 | public AppException(string message) 23 | : this(ApiResultStatusCode.ServerError, message) 24 | { 25 | } 26 | 27 | public AppException(ApiResultStatusCode statusCode, string message) 28 | : this(statusCode, message, HttpStatusCode.InternalServerError) 29 | { 30 | } 31 | 32 | public AppException(string message, object additionalData) 33 | : this(ApiResultStatusCode.ServerError, message, additionalData) 34 | { 35 | } 36 | 37 | public AppException(ApiResultStatusCode statusCode, object additionalData) 38 | : this(statusCode, null, additionalData) 39 | { 40 | } 41 | 42 | public AppException(ApiResultStatusCode statusCode, string message, object additionalData) 43 | : this(statusCode, message, HttpStatusCode.InternalServerError, additionalData) 44 | { 45 | } 46 | 47 | public AppException(ApiResultStatusCode statusCode, string message, HttpStatusCode httpStatusCode) 48 | : this(statusCode, message, httpStatusCode, null) 49 | { 50 | } 51 | 52 | public AppException(ApiResultStatusCode statusCode, string message, HttpStatusCode httpStatusCode, object additionalData) 53 | : this(statusCode, message, httpStatusCode, null, additionalData) 54 | { 55 | } 56 | 57 | public AppException(string message, Exception exception) 58 | : this(ApiResultStatusCode.ServerError, message, exception) 59 | { 60 | } 61 | 62 | public AppException(string message, Exception exception, object additionalData) 63 | : this(ApiResultStatusCode.ServerError, message, exception, additionalData) 64 | { 65 | } 66 | 67 | public AppException(ApiResultStatusCode statusCode, string message, Exception exception) 68 | : this(statusCode, message, HttpStatusCode.InternalServerError, exception) 69 | { 70 | } 71 | 72 | public AppException(ApiResultStatusCode statusCode, string message, Exception exception, object additionalData) 73 | : this(statusCode, message, HttpStatusCode.InternalServerError, exception, additionalData) 74 | { 75 | } 76 | 77 | public AppException(ApiResultStatusCode statusCode, string message, HttpStatusCode httpStatusCode, Exception exception) 78 | : this(statusCode, message, httpStatusCode, exception, null) 79 | { 80 | } 81 | 82 | public AppException(ApiResultStatusCode statusCode, string message, HttpStatusCode httpStatusCode, Exception exception, object additionalData) 83 | : base(message, exception) 84 | { 85 | ApiStatusCode = statusCode; 86 | HttpStatusCode = httpStatusCode; 87 | AdditionalData = additionalData; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Common/Exceptions/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Common.Exceptions 4 | { 5 | public class BadRequestException : AppException 6 | { 7 | public BadRequestException() 8 | : base(ApiResultStatusCode.BadRequest, System.Net.HttpStatusCode.BadRequest) 9 | { 10 | } 11 | 12 | public BadRequestException(string message) 13 | : base(ApiResultStatusCode.BadRequest, message, System.Net.HttpStatusCode.BadRequest) 14 | { 15 | } 16 | 17 | public BadRequestException(object additionalData) 18 | : base(ApiResultStatusCode.BadRequest, null, System.Net.HttpStatusCode.BadRequest, additionalData) 19 | { 20 | } 21 | 22 | public BadRequestException(string message, object additionalData) 23 | : base(ApiResultStatusCode.BadRequest, message, System.Net.HttpStatusCode.BadRequest, additionalData) 24 | { 25 | } 26 | 27 | public BadRequestException(string message, Exception exception) 28 | : base(ApiResultStatusCode.BadRequest, message, exception, System.Net.HttpStatusCode.BadRequest) 29 | { 30 | } 31 | 32 | public BadRequestException(string message, Exception exception, object additionalData) 33 | : base(ApiResultStatusCode.BadRequest, message, System.Net.HttpStatusCode.BadRequest, exception, additionalData) 34 | { 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Common/Exceptions/LogicException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Common.Exceptions 4 | { 5 | public class LogicException : AppException 6 | { 7 | public LogicException() 8 | : base(ApiResultStatusCode.LogicError) 9 | { 10 | } 11 | 12 | public LogicException(string message) 13 | : base(ApiResultStatusCode.LogicError, message) 14 | { 15 | } 16 | 17 | public LogicException(object additionalData) 18 | : base(ApiResultStatusCode.LogicError, additionalData) 19 | { 20 | } 21 | 22 | public LogicException(string message, object additionalData) 23 | : base(ApiResultStatusCode.LogicError, message, additionalData) 24 | { 25 | } 26 | 27 | public LogicException(string message, Exception exception) 28 | : base(ApiResultStatusCode.LogicError, message, exception) 29 | { 30 | } 31 | 32 | public LogicException(string message, Exception exception, object additionalData) 33 | : base(ApiResultStatusCode.LogicError, message, exception, additionalData) 34 | { 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Common/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Common.Exceptions 4 | { 5 | public class NotFoundException : AppException 6 | { 7 | public NotFoundException() 8 | : base(ApiResultStatusCode.NotFound, System.Net.HttpStatusCode.NotFound) 9 | { 10 | } 11 | 12 | public NotFoundException(string message) 13 | : base(ApiResultStatusCode.NotFound, message, System.Net.HttpStatusCode.NotFound) 14 | { 15 | } 16 | 17 | public NotFoundException(object additionalData) 18 | : base(ApiResultStatusCode.NotFound, null, System.Net.HttpStatusCode.NotFound, additionalData) 19 | { 20 | } 21 | 22 | public NotFoundException(string message, object additionalData) 23 | : base(ApiResultStatusCode.NotFound, message, System.Net.HttpStatusCode.NotFound, additionalData) 24 | { 25 | } 26 | 27 | public NotFoundException(string message, Exception exception) 28 | : base(ApiResultStatusCode.NotFound, message, exception, System.Net.HttpStatusCode.NotFound) 29 | { 30 | } 31 | 32 | public NotFoundException(string message, Exception exception, object additionalData) 33 | : base(ApiResultStatusCode.NotFound, message, System.Net.HttpStatusCode.NotFound, exception, additionalData) 34 | { 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Common/IScopedDependency.cs: -------------------------------------------------------------------------------- 1 | namespace Common 2 | { 3 | //just to mark 4 | public interface IScopedDependency 5 | { 6 | } 7 | 8 | public interface ITransientDependency 9 | { 10 | } 11 | 12 | public interface ISingletonDependency 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Common/SiteSettings.cs: -------------------------------------------------------------------------------- 1 | namespace Common 2 | { 3 | public class SiteSettings 4 | { 5 | public string ElmahPath { get; set; } 6 | public JwtSettings JwtSettings { get; set; } 7 | public IdentitySettings IdentitySettings { get; set; } 8 | } 9 | 10 | public class IdentitySettings 11 | { 12 | public bool PasswordRequireDigit { get; set; } 13 | public int PasswordRequiredLength { get; set; } 14 | public bool PasswordRequireNonAlphanumeric { get; set; } 15 | public bool PasswordRequireUppercase { get; set; } 16 | public bool PasswordRequireLowercase { get; set; } 17 | public bool RequireUniqueEmail { get; set; } 18 | } 19 | public class JwtSettings 20 | { 21 | public string SecretKey { get; set; } 22 | public string EncryptKey { get; set; } 23 | public string Issuer { get; set; } 24 | public string Audience { get; set; } 25 | public int NotBeforeMinutes { get; set; } 26 | public int ExpirationMinutes { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Common/Utilities/Assert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Linq; 4 | 5 | namespace Common.Utilities 6 | { 7 | public static class Assert 8 | { 9 | public static void NotNull(T obj, string name, string message = null) 10 | where T : class 11 | { 12 | if (obj is null) 13 | throw new ArgumentNullException($"{name} : {typeof(T)}" , message); 14 | } 15 | 16 | public static void NotNull(T? obj, string name, string message = null) 17 | where T : struct 18 | { 19 | if (!obj.HasValue) 20 | throw new ArgumentNullException($"{name} : {typeof(T)}", message); 21 | 22 | } 23 | 24 | public static void NotEmpty(T obj, string name, string message = null, T defaultValue = null) 25 | where T : class 26 | { 27 | if (obj == defaultValue 28 | || (obj is string str && string.IsNullOrWhiteSpace(str)) 29 | || (obj is IEnumerable list && !list.Cast().Any())) 30 | { 31 | throw new ArgumentException("Argument is empty : " + message, $"{name} : {typeof(T)}"); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Common/Utilities/EnumExcentions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace Common.Utilities 8 | { 9 | public static class EnumExtensions 10 | { 11 | public static IEnumerable GetEnumValues(this T input) where T : struct 12 | { 13 | if (!typeof(T).IsEnum) 14 | throw new NotSupportedException(); 15 | 16 | return Enum.GetValues(input.GetType()).Cast(); 17 | } 18 | 19 | public static IEnumerable GetEnumFlags(this T input) where T : struct 20 | { 21 | if (!typeof(T).IsEnum) 22 | throw new NotSupportedException(); 23 | 24 | foreach (var value in Enum.GetValues(input.GetType())) 25 | if ((input as Enum).HasFlag(value as Enum)) 26 | yield return (T)value; 27 | } 28 | 29 | public static string ToDisplay(this Enum value, DisplayProperty property = DisplayProperty.Name) 30 | { 31 | Assert.NotNull(value, nameof(value)); 32 | 33 | var attribute = value.GetType().GetField(value.ToString()) 34 | .GetCustomAttributes(false).FirstOrDefault(); 35 | 36 | if (attribute == null) 37 | return value.ToString(); 38 | 39 | var propValue = attribute.GetType().GetProperty(property.ToString()).GetValue(attribute, null); 40 | return propValue.ToString(); 41 | } 42 | 43 | public static Dictionary ToDictionary(this Enum value) 44 | { 45 | return Enum.GetValues(value.GetType()).Cast().ToDictionary(p => Convert.ToInt32(p), q => ToDisplay(q)); 46 | } 47 | } 48 | 49 | public enum DisplayProperty 50 | { 51 | Description, 52 | GroupName, 53 | Name, 54 | Prompt, 55 | ShortName, 56 | Order 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Common/Utilities/IdentityExtensions.cs: -------------------------------------------------------------------------------- 1 | using Common.Utilities; 2 | using System; 3 | using System.Globalization; 4 | using System.Security.Claims; 5 | using System.Security.Principal; 6 | 7 | namespace Common 8 | { 9 | public static class IdentityExtensions 10 | { 11 | public static string FindFirstValue(this ClaimsIdentity identity, string claimType) 12 | { 13 | return identity?.FindFirst(claimType)?.Value; 14 | } 15 | 16 | public static string FindFirstValue(this IIdentity identity, string claimType) 17 | { 18 | var claimsIdentity = identity as ClaimsIdentity; 19 | return claimsIdentity?.FindFirstValue(claimType); 20 | } 21 | 22 | public static string GetUserId(this IIdentity identity) 23 | { 24 | return identity?.FindFirstValue(ClaimTypes.NameIdentifier); 25 | } 26 | 27 | public static T GetUserId(this IIdentity identity) where T : IConvertible 28 | { 29 | var userId = identity?.GetUserId(); 30 | return userId.HasValue() 31 | ? (T)Convert.ChangeType(userId, typeof(T), CultureInfo.InvariantCulture) 32 | : default(T); 33 | } 34 | 35 | public static string GetUserName(this IIdentity identity) 36 | { 37 | return identity?.FindFirstValue(ClaimTypes.Name); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Common/Utilities/ModelBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata; 3 | using Pluralize.NET; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Reflection; 8 | 9 | namespace Common.Utilities 10 | { 11 | public static class ModelBuilderExtensions 12 | { 13 | /// 14 | /// Singularizin table name like Posts to Post or People to Person 15 | /// 16 | /// 17 | public static void AddSingularizingTableNameConvention(this ModelBuilder modelBuilder) 18 | { 19 | Pluralizer pluralizer = new Pluralizer(); 20 | foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) 21 | { 22 | string tableName = entityType.GetTableName(); 23 | entityType.SetTableName(pluralizer.Singularize(tableName)); 24 | } 25 | } 26 | 27 | /// 28 | /// Pluralizing table name like Post to Posts or Person to People 29 | /// 30 | /// 31 | public static void AddPluralizingTableNameConvention(this ModelBuilder modelBuilder) 32 | { 33 | Pluralizer pluralizer = new Pluralizer(); 34 | foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) 35 | { 36 | string tableName = entityType.GetTableName(); 37 | entityType.SetTableName(pluralizer.Pluralize(tableName)); 38 | } 39 | } 40 | 41 | /// 42 | /// Set NEWSEQUENTIALID() sql function for all columns named "Id" 43 | /// 44 | /// 45 | /// Set to true if you want only "Identity" guid fields that named "Id" 46 | public static void AddSequentialGuidForIdConvention(this ModelBuilder modelBuilder) 47 | { 48 | modelBuilder.AddDefaultValueSqlConvention("Id", typeof(Guid), "NEWSEQUENTIALID()"); 49 | } 50 | 51 | /// 52 | /// Set DefaultValueSql for sepecific property name and type 53 | /// 54 | /// 55 | /// Name of property wants to set DefaultValueSql for 56 | /// Type of property wants to set DefaultValueSql for 57 | /// DefaultValueSql like "NEWSEQUENTIALID()" 58 | public static void AddDefaultValueSqlConvention(this ModelBuilder modelBuilder, string propertyName, Type propertyType, string defaultValueSql) 59 | { 60 | foreach (IMutableEntityType entityType in modelBuilder.Model.GetEntityTypes()) 61 | { 62 | IMutableProperty property = entityType.GetProperties().SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase)); 63 | if (property != null && property.ClrType == propertyType) 64 | property.SetDefaultValueSql(defaultValueSql); 65 | } 66 | } 67 | 68 | /// 69 | /// Set DeleteBehavior.Restrict by default for relations 70 | /// 71 | /// 72 | public static void AddRestrictDeleteBehaviorConvention(this ModelBuilder modelBuilder) 73 | { 74 | IEnumerable cascadeFKs = modelBuilder.Model.GetEntityTypes() 75 | .SelectMany(t => t.GetForeignKeys()) 76 | .Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade); 77 | foreach (IMutableForeignKey fk in cascadeFKs) 78 | fk.DeleteBehavior = DeleteBehavior.Restrict; 79 | } 80 | 81 | /// 82 | /// Dynamicaly load all IEntityTypeConfiguration with Reflection 83 | /// 84 | /// 85 | /// Assemblies contains Entities 86 | public static void RegisterEntityTypeConfiguration(this ModelBuilder modelBuilder, params Assembly[] assemblies) 87 | { 88 | MethodInfo applyGenericMethod = typeof(ModelBuilder).GetMethods().First(m => m.Name == nameof(ModelBuilder.ApplyConfiguration)); 89 | 90 | IEnumerable types = assemblies.SelectMany(a => a.GetExportedTypes()) 91 | .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic); 92 | 93 | foreach (Type type in types) 94 | { 95 | foreach (Type iface in type.GetInterfaces()) 96 | { 97 | if (iface.IsConstructedGenericType && iface.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>)) 98 | { 99 | MethodInfo applyConcreteMethod = applyGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]); 100 | applyConcreteMethod.Invoke(modelBuilder, new object[] { Activator.CreateInstance(type) }); 101 | } 102 | } 103 | } 104 | } 105 | 106 | /// 107 | /// Dynamicaly register all Entities that inherit from specific BaseType 108 | /// 109 | /// 110 | /// Base type that Entities inherit from this 111 | /// Assemblies contains Entities 112 | public static void RegisterAllEntities(this ModelBuilder modelBuilder, params Assembly[] assemblies) 113 | { 114 | IEnumerable types = assemblies.SelectMany(a => a.GetExportedTypes()) 115 | .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(BaseType).IsAssignableFrom(c)); 116 | 117 | foreach (Type type in types) 118 | modelBuilder.Entity(type); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Common/Utilities/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace Common.Utilities 8 | { 9 | public static class ReflectionHelper 10 | { 11 | public static bool HasAttribute(this MemberInfo type, bool inherit = false) where T : Attribute 12 | { 13 | return HasAttribute(type, typeof(T), inherit); 14 | } 15 | 16 | public static bool HasAttribute(this MemberInfo type, Type attribute, bool inherit = false) 17 | { 18 | return Attribute.IsDefined(type, attribute, inherit); 19 | //return type.IsDefined(attribute, inherit); 20 | //return type.GetCustomAttributes(attribute, inherit).Length > 0; 21 | } 22 | 23 | public static bool IsInheritFrom(this Type type) 24 | { 25 | return IsInheritFrom(type, typeof(T)); 26 | } 27 | 28 | public static bool IsInheritFrom(this Type type, Type parentType) 29 | { 30 | //the 'is' keyword do this too for values (new ChildClass() is ParentClass) 31 | return parentType.IsAssignableFrom(type); 32 | } 33 | 34 | public static bool BaseTypeIsGeneric(this Type type, Type genericType) 35 | { 36 | return type.BaseType?.IsGenericType == true && type.BaseType.GetGenericTypeDefinition() == genericType; 37 | } 38 | 39 | public static IEnumerable GetTypesAssignableFrom(params Assembly[] assemblies) 40 | { 41 | return typeof(T).GetTypesAssignableFrom(assemblies); 42 | } 43 | 44 | public static IEnumerable GetTypesAssignableFrom(this Type type, params Assembly[] assemblies) 45 | { 46 | return assemblies.SelectMany(p => p.GetTypes()).Where(p => p.IsInheritFrom(type)); 47 | } 48 | 49 | public static IEnumerable GetTypesHasAttribute(params Assembly[] assemblies) where T : Attribute 50 | { 51 | return typeof(T).GetTypesHasAttribute(assemblies); 52 | } 53 | 54 | public static IEnumerable GetTypesHasAttribute(this Type type, params Assembly[] assemblies) 55 | { 56 | return assemblies.SelectMany(p => p.GetTypes()).Where(p => p.HasAttribute(type)); 57 | } 58 | 59 | public static bool IsEnumerable(this Type type) 60 | { 61 | return type != typeof(string) && type.IsInheritFrom(); 62 | } 63 | 64 | public static bool IsEnumerable(this Type type) 65 | { 66 | return type != typeof(string) && type.IsInheritFrom>() && type.IsGenericType; 67 | } 68 | 69 | public static IEnumerable GetBaseTypesAndInterfaces(this Type type) 70 | { 71 | if ((type == null) || (type.BaseType == null)) 72 | yield break; 73 | 74 | foreach (var i in type.GetInterfaces()) 75 | yield return i; 76 | 77 | var currentBaseType = type.BaseType; 78 | while (currentBaseType != null) 79 | { 80 | yield return currentBaseType; 81 | currentBaseType = currentBaseType.BaseType; 82 | } 83 | } 84 | 85 | public static bool IsCustomType(this Type type) 86 | { 87 | //return type.Assembly.GetName().Name != "mscorlib"; 88 | return type.IsCustomValueType() || type.IsCustomReferenceType(); 89 | } 90 | 91 | public static bool IsCustomValueType(this Type type) 92 | { 93 | return type.IsValueType && !type.IsPrimitive && type.Namespace != null && !type.Namespace.StartsWith("System", StringComparison.Ordinal); 94 | } 95 | 96 | public static bool IsCustomReferenceType(this Type type) 97 | { 98 | return !type.IsValueType && !type.IsPrimitive && type.Namespace != null && !type.Namespace.StartsWith("System", StringComparison.Ordinal); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Common/Utilities/SecurityHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography; 3 | using System.Text; 4 | 5 | namespace Common.Utilities 6 | { 7 | public static class SecurityHelper 8 | { 9 | public static string GetSha256Hash(string input) 10 | { 11 | //using (var sha256 = new SHA256CryptoServiceProvider()) 12 | using (var sha256 = SHA256.Create()) 13 | { 14 | var byteValue = Encoding.UTF8.GetBytes(input); 15 | var byteHash = sha256.ComputeHash(byteValue); 16 | return Convert.ToBase64String(byteHash); 17 | //return BitConverter.ToString(byteHash).Replace("-", "").ToLower(); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Common/Utilities/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Common.Utilities 4 | { 5 | public static class StringExtensions 6 | { 7 | public static bool HasValue(this string value, bool ignoreWhiteSpace = true) 8 | { 9 | return ignoreWhiteSpace ? !string.IsNullOrWhiteSpace(value) : !string.IsNullOrEmpty(value); 10 | } 11 | 12 | public static int ToInt(this string value) 13 | { 14 | return Convert.ToInt32(value); 15 | } 16 | 17 | public static decimal ToDecimal(this string value) 18 | { 19 | return Convert.ToDecimal(value); 20 | } 21 | 22 | public static string ToNumeric(this int value) 23 | { 24 | return value.ToString("N0"); //"123,456" 25 | } 26 | 27 | public static string ToNumeric(this decimal value) 28 | { 29 | return value.ToString("N0"); 30 | } 31 | 32 | public static string ToCurrency(this int value) 33 | { 34 | //fa-IR => current culture currency symbol => ریال 35 | //123456 => "123,123ریال" 36 | return value.ToString("C0"); 37 | } 38 | 39 | public static string ToCurrency(this decimal value) 40 | { 41 | return value.ToString("C0"); 42 | } 43 | 44 | public static string En2Fa(this string str) 45 | { 46 | return str.Replace("0", "۰") 47 | .Replace("1", "۱") 48 | .Replace("2", "۲") 49 | .Replace("3", "۳") 50 | .Replace("4", "۴") 51 | .Replace("5", "۵") 52 | .Replace("6", "۶") 53 | .Replace("7", "۷") 54 | .Replace("8", "۸") 55 | .Replace("9", "۹"); 56 | } 57 | 58 | public static string Fa2En(this string str) 59 | { 60 | return str.Replace("۰", "0") 61 | .Replace("۱", "1") 62 | .Replace("۲", "2") 63 | .Replace("۳", "3") 64 | .Replace("۴", "4") 65 | .Replace("۵", "5") 66 | .Replace("۶", "6") 67 | .Replace("۷", "7") 68 | .Replace("۸", "8") 69 | .Replace("۹", "9") 70 | //iphone numeric 71 | .Replace("٠", "0") 72 | .Replace("١", "1") 73 | .Replace("٢", "2") 74 | .Replace("٣", "3") 75 | .Replace("٤", "4") 76 | .Replace("٥", "5") 77 | .Replace("٦", "6") 78 | .Replace("٧", "7") 79 | .Replace("٨", "8") 80 | .Replace("٩", "9"); 81 | } 82 | 83 | public static string FixPersianChars(this string str) 84 | { 85 | return str.Replace("ﮎ", "ک") 86 | .Replace("ﮏ", "ک") 87 | .Replace("ﮐ", "ک") 88 | .Replace("ﮑ", "ک") 89 | .Replace("ك", "ک") 90 | .Replace("ي", "ی") 91 | .Replace(" ", " ") 92 | .Replace("‌", " ") 93 | .Replace("ھ", "ه");//.Replace("ئ", "ی"); 94 | } 95 | 96 | public static string CleanString(this string str) 97 | { 98 | return str.Trim().FixPersianChars().Fa2En().NullIfEmpty(); 99 | } 100 | 101 | public static string NullIfEmpty(this string str) 102 | { 103 | return str?.Length == 0 ? null : str; 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Data/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Common.Utilities; 2 | using Entities; 3 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Data 11 | { 12 | public class ApplicationDbContext : IdentityDbContext 13 | //DbContext 14 | { 15 | public ApplicationDbContext(DbContextOptions options) 16 | : base(options) 17 | { 18 | 19 | } 20 | 21 | //protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 22 | //{ 23 | // optionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=MyApiDb;Integrated Security=true"); 24 | // base.OnConfiguring(optionsBuilder); 25 | //} 26 | 27 | protected override void OnModelCreating(ModelBuilder modelBuilder) 28 | { 29 | base.OnModelCreating(modelBuilder); 30 | 31 | var entitiesAssembly = typeof(IEntity).Assembly; 32 | 33 | modelBuilder.RegisterAllEntities(entitiesAssembly); 34 | modelBuilder.RegisterEntityTypeConfiguration(entitiesAssembly); 35 | modelBuilder.AddRestrictDeleteBehaviorConvention(); 36 | modelBuilder.AddSequentialGuidForIdConvention(); 37 | modelBuilder.AddPluralizingTableNameConvention(); 38 | } 39 | 40 | public override int SaveChanges() 41 | { 42 | _cleanString(); 43 | return base.SaveChanges(); 44 | } 45 | 46 | public override int SaveChanges(bool acceptAllChangesOnSuccess) 47 | { 48 | _cleanString(); 49 | return base.SaveChanges(acceptAllChangesOnSuccess); 50 | } 51 | 52 | public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) 53 | { 54 | _cleanString(); 55 | return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); 56 | } 57 | 58 | public override Task SaveChangesAsync(CancellationToken cancellationToken = default) 59 | { 60 | _cleanString(); 61 | return base.SaveChangesAsync(cancellationToken); 62 | } 63 | 64 | private void _cleanString() 65 | { 66 | var changedEntities = ChangeTracker.Entries() 67 | .Where(x => x.State == EntityState.Added || x.State == EntityState.Modified); 68 | foreach (var item in changedEntities) 69 | { 70 | if (item.Entity == null) 71 | continue; 72 | 73 | var properties = item.Entity.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance) 74 | .Where(p => p.CanRead && p.CanWrite && p.PropertyType == typeof(string)); 75 | 76 | foreach (var property in properties) 77 | { 78 | var propName = property.Name; 79 | var val = (string)property.GetValue(item.Entity, null); 80 | 81 | if (val.HasValue()) 82 | { 83 | var newVal = val.Fa2En().FixPersianChars(); 84 | if (newVal == val) 85 | continue; 86 | property.SetValue(item.Entity, newVal, null); 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Data/Contracts/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Entities; 8 | using Microsoft.EntityFrameworkCore; 9 | 10 | namespace Data.Repositories 11 | { 12 | public interface IRepository where TEntity : class, IEntity 13 | { 14 | DbSet Entities { get; } 15 | IQueryable Table { get; } 16 | IQueryable TableNoTracking { get; } 17 | 18 | void Add(TEntity entity, bool saveNow = true); 19 | Task AddAsync(TEntity entity, CancellationToken cancellationToken, bool saveNow = true); 20 | void AddRange(IEnumerable entities, bool saveNow = true); 21 | Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken, bool saveNow = true); 22 | void Attach(TEntity entity); 23 | void Delete(TEntity entity, bool saveNow = true); 24 | Task DeleteAsync(TEntity entity, CancellationToken cancellationToken, bool saveNow = true); 25 | void DeleteRange(IEnumerable entities, bool saveNow = true); 26 | Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken, bool saveNow = true); 27 | void Detach(TEntity entity); 28 | TEntity GetById(params object[] ids); 29 | ValueTask GetByIdAsync(CancellationToken cancellationToken, params object[] ids); 30 | void LoadCollection(TEntity entity, Expression>> collectionProperty) where TProperty : class; 31 | Task LoadCollectionAsync(TEntity entity, Expression>> collectionProperty, CancellationToken cancellationToken) where TProperty : class; 32 | void LoadReference(TEntity entity, Expression> referenceProperty) where TProperty : class; 33 | Task LoadReferenceAsync(TEntity entity, Expression> referenceProperty, CancellationToken cancellationToken) where TProperty : class; 34 | void Update(TEntity entity, bool saveNow = true); 35 | Task UpdateAsync(TEntity entity, CancellationToken cancellationToken, bool saveNow = true); 36 | void UpdateRange(IEnumerable entities, bool saveNow = true); 37 | Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken, bool saveNow = true); 38 | } 39 | } -------------------------------------------------------------------------------- /Data/Contracts/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using Entities; 4 | 5 | namespace Data.Repositories 6 | { 7 | public interface IUserRepository : IRepository 8 | { 9 | Task GetByUserAndPass(string username, string password, CancellationToken cancellationToken); 10 | 11 | Task AddAsync(User user, string password, CancellationToken cancellationToken); 12 | Task UpdateSecurityStampAsync(User user, CancellationToken cancellationToken); 13 | Task UpdateLastLoginDateAsync(User user, CancellationToken cancellationToken); 14 | } 15 | } -------------------------------------------------------------------------------- /Data/Data.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | 9.0 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Data/Migrations/20190209131717_initial database.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace Data.Migrations 11 | { 12 | [DbContext(typeof(ApplicationDbContext))] 13 | [Migration("20190209131717_initial database")] 14 | partial class initialdatabase 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "2.1.4-rtm-31024") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("Entities.Category", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 29 | 30 | b.Property("Name") 31 | .IsRequired() 32 | .HasMaxLength(50); 33 | 34 | b.Property("ParentCategoryId"); 35 | 36 | b.HasKey("Id"); 37 | 38 | b.HasIndex("ParentCategoryId"); 39 | 40 | b.ToTable("Categories"); 41 | }); 42 | 43 | modelBuilder.Entity("Entities.Post", b => 44 | { 45 | b.Property("Id") 46 | .ValueGeneratedOnAdd() 47 | .HasDefaultValueSql("NEWSEQUENTIALID()"); 48 | 49 | b.Property("AuthorId"); 50 | 51 | b.Property("CategoryId"); 52 | 53 | b.Property("Description") 54 | .IsRequired(); 55 | 56 | b.Property("Title") 57 | .IsRequired() 58 | .HasMaxLength(200); 59 | 60 | b.HasKey("Id"); 61 | 62 | b.HasIndex("AuthorId"); 63 | 64 | b.HasIndex("CategoryId"); 65 | 66 | b.ToTable("Posts"); 67 | }); 68 | 69 | modelBuilder.Entity("Entities.Role", b => 70 | { 71 | b.Property("Id") 72 | .ValueGeneratedOnAdd() 73 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 74 | 75 | b.Property("ConcurrencyStamp") 76 | .IsConcurrencyToken(); 77 | 78 | b.Property("Description") 79 | .IsRequired() 80 | .HasMaxLength(100); 81 | 82 | b.Property("Name") 83 | .IsRequired() 84 | .HasMaxLength(50); 85 | 86 | b.Property("NormalizedName") 87 | .HasMaxLength(256); 88 | 89 | b.HasKey("Id"); 90 | 91 | b.HasIndex("NormalizedName") 92 | .IsUnique() 93 | .HasName("RoleNameIndex") 94 | .HasFilter("[NormalizedName] IS NOT NULL"); 95 | 96 | b.ToTable("AspNetRoles"); 97 | }); 98 | 99 | modelBuilder.Entity("Entities.User", b => 100 | { 101 | b.Property("Id") 102 | .ValueGeneratedOnAdd() 103 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 104 | 105 | b.Property("AccessFailedCount"); 106 | 107 | b.Property("Age"); 108 | 109 | b.Property("ConcurrencyStamp") 110 | .IsConcurrencyToken(); 111 | 112 | b.Property("Email") 113 | .HasMaxLength(256); 114 | 115 | b.Property("EmailConfirmed"); 116 | 117 | b.Property("FullName") 118 | .IsRequired() 119 | .HasMaxLength(100); 120 | 121 | b.Property("Gender"); 122 | 123 | b.Property("IsActive"); 124 | 125 | b.Property("LastLoginDate"); 126 | 127 | b.Property("LockoutEnabled"); 128 | 129 | b.Property("LockoutEnd"); 130 | 131 | b.Property("NormalizedEmail") 132 | .HasMaxLength(256); 133 | 134 | b.Property("NormalizedUserName") 135 | .HasMaxLength(256); 136 | 137 | b.Property("PasswordHash"); 138 | 139 | b.Property("PhoneNumber"); 140 | 141 | b.Property("PhoneNumberConfirmed"); 142 | 143 | b.Property("SecurityStamp"); 144 | 145 | b.Property("TwoFactorEnabled"); 146 | 147 | b.Property("UserName") 148 | .IsRequired() 149 | .HasMaxLength(100); 150 | 151 | b.HasKey("Id"); 152 | 153 | b.HasIndex("NormalizedEmail") 154 | .HasName("EmailIndex"); 155 | 156 | b.HasIndex("NormalizedUserName") 157 | .IsUnique() 158 | .HasName("UserNameIndex") 159 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 160 | 161 | b.ToTable("AspNetUsers"); 162 | }); 163 | 164 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 165 | { 166 | b.Property("Id") 167 | .ValueGeneratedOnAdd() 168 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 169 | 170 | b.Property("ClaimType"); 171 | 172 | b.Property("ClaimValue"); 173 | 174 | b.Property("RoleId"); 175 | 176 | b.HasKey("Id"); 177 | 178 | b.HasIndex("RoleId"); 179 | 180 | b.ToTable("AspNetRoleClaims"); 181 | }); 182 | 183 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 184 | { 185 | b.Property("Id") 186 | .ValueGeneratedOnAdd() 187 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 188 | 189 | b.Property("ClaimType"); 190 | 191 | b.Property("ClaimValue"); 192 | 193 | b.Property("UserId"); 194 | 195 | b.HasKey("Id"); 196 | 197 | b.HasIndex("UserId"); 198 | 199 | b.ToTable("AspNetUserClaims"); 200 | }); 201 | 202 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 203 | { 204 | b.Property("LoginProvider"); 205 | 206 | b.Property("ProviderKey"); 207 | 208 | b.Property("ProviderDisplayName"); 209 | 210 | b.Property("UserId"); 211 | 212 | b.HasKey("LoginProvider", "ProviderKey"); 213 | 214 | b.HasIndex("UserId"); 215 | 216 | b.ToTable("AspNetUserLogins"); 217 | }); 218 | 219 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 220 | { 221 | b.Property("UserId"); 222 | 223 | b.Property("RoleId"); 224 | 225 | b.HasKey("UserId", "RoleId"); 226 | 227 | b.HasIndex("RoleId"); 228 | 229 | b.ToTable("AspNetUserRoles"); 230 | }); 231 | 232 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 233 | { 234 | b.Property("UserId"); 235 | 236 | b.Property("LoginProvider"); 237 | 238 | b.Property("Name"); 239 | 240 | b.Property("Value"); 241 | 242 | b.HasKey("UserId", "LoginProvider", "Name"); 243 | 244 | b.ToTable("AspNetUserTokens"); 245 | }); 246 | 247 | modelBuilder.Entity("Entities.Category", b => 248 | { 249 | b.HasOne("Entities.Category", "ParentCategory") 250 | .WithMany("ChildCategories") 251 | .HasForeignKey("ParentCategoryId"); 252 | }); 253 | 254 | modelBuilder.Entity("Entities.Post", b => 255 | { 256 | b.HasOne("Entities.User", "Author") 257 | .WithMany("Posts") 258 | .HasForeignKey("AuthorId") 259 | .OnDelete(DeleteBehavior.Restrict); 260 | 261 | b.HasOne("Entities.Category", "Category") 262 | .WithMany("Posts") 263 | .HasForeignKey("CategoryId") 264 | .OnDelete(DeleteBehavior.Restrict); 265 | }); 266 | 267 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 268 | { 269 | b.HasOne("Entities.Role") 270 | .WithMany() 271 | .HasForeignKey("RoleId") 272 | .OnDelete(DeleteBehavior.Restrict); 273 | }); 274 | 275 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 276 | { 277 | b.HasOne("Entities.User") 278 | .WithMany() 279 | .HasForeignKey("UserId") 280 | .OnDelete(DeleteBehavior.Restrict); 281 | }); 282 | 283 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 284 | { 285 | b.HasOne("Entities.User") 286 | .WithMany() 287 | .HasForeignKey("UserId") 288 | .OnDelete(DeleteBehavior.Restrict); 289 | }); 290 | 291 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 292 | { 293 | b.HasOne("Entities.Role") 294 | .WithMany() 295 | .HasForeignKey("RoleId") 296 | .OnDelete(DeleteBehavior.Restrict); 297 | 298 | b.HasOne("Entities.User") 299 | .WithMany() 300 | .HasForeignKey("UserId") 301 | .OnDelete(DeleteBehavior.Restrict); 302 | }); 303 | 304 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 305 | { 306 | b.HasOne("Entities.User") 307 | .WithMany() 308 | .HasForeignKey("UserId") 309 | .OnDelete(DeleteBehavior.Restrict); 310 | }); 311 | #pragma warning restore 612, 618 312 | } 313 | } 314 | } 315 | -------------------------------------------------------------------------------- /Data/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace Data.Migrations 10 | { 11 | [DbContext(typeof(ApplicationDbContext))] 12 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "2.1.4-rtm-31024") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("Entities.Category", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 27 | 28 | b.Property("Name") 29 | .IsRequired() 30 | .HasMaxLength(50); 31 | 32 | b.Property("ParentCategoryId"); 33 | 34 | b.HasKey("Id"); 35 | 36 | b.HasIndex("ParentCategoryId"); 37 | 38 | b.ToTable("Categories"); 39 | }); 40 | 41 | modelBuilder.Entity("Entities.Post", b => 42 | { 43 | b.Property("Id") 44 | .ValueGeneratedOnAdd() 45 | .HasDefaultValueSql("NEWSEQUENTIALID()"); 46 | 47 | b.Property("AuthorId"); 48 | 49 | b.Property("CategoryId"); 50 | 51 | b.Property("Description") 52 | .IsRequired(); 53 | 54 | b.Property("Title") 55 | .IsRequired() 56 | .HasMaxLength(200); 57 | 58 | b.HasKey("Id"); 59 | 60 | b.HasIndex("AuthorId"); 61 | 62 | b.HasIndex("CategoryId"); 63 | 64 | b.ToTable("Posts"); 65 | }); 66 | 67 | modelBuilder.Entity("Entities.Role", b => 68 | { 69 | b.Property("Id") 70 | .ValueGeneratedOnAdd() 71 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 72 | 73 | b.Property("ConcurrencyStamp") 74 | .IsConcurrencyToken(); 75 | 76 | b.Property("Description") 77 | .IsRequired() 78 | .HasMaxLength(100); 79 | 80 | b.Property("Name") 81 | .IsRequired() 82 | .HasMaxLength(50); 83 | 84 | b.Property("NormalizedName") 85 | .HasMaxLength(256); 86 | 87 | b.HasKey("Id"); 88 | 89 | b.HasIndex("NormalizedName") 90 | .IsUnique() 91 | .HasName("RoleNameIndex") 92 | .HasFilter("[NormalizedName] IS NOT NULL"); 93 | 94 | b.ToTable("AspNetRoles"); 95 | }); 96 | 97 | modelBuilder.Entity("Entities.User", b => 98 | { 99 | b.Property("Id") 100 | .ValueGeneratedOnAdd() 101 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 102 | 103 | b.Property("AccessFailedCount"); 104 | 105 | b.Property("Age"); 106 | 107 | b.Property("ConcurrencyStamp") 108 | .IsConcurrencyToken(); 109 | 110 | b.Property("Email") 111 | .HasMaxLength(256); 112 | 113 | b.Property("EmailConfirmed"); 114 | 115 | b.Property("FullName") 116 | .IsRequired() 117 | .HasMaxLength(100); 118 | 119 | b.Property("Gender"); 120 | 121 | b.Property("IsActive"); 122 | 123 | b.Property("LastLoginDate"); 124 | 125 | b.Property("LockoutEnabled"); 126 | 127 | b.Property("LockoutEnd"); 128 | 129 | b.Property("NormalizedEmail") 130 | .HasMaxLength(256); 131 | 132 | b.Property("NormalizedUserName") 133 | .HasMaxLength(256); 134 | 135 | b.Property("PasswordHash"); 136 | 137 | b.Property("PhoneNumber"); 138 | 139 | b.Property("PhoneNumberConfirmed"); 140 | 141 | b.Property("SecurityStamp"); 142 | 143 | b.Property("TwoFactorEnabled"); 144 | 145 | b.Property("UserName") 146 | .IsRequired() 147 | .HasMaxLength(100); 148 | 149 | b.HasKey("Id"); 150 | 151 | b.HasIndex("NormalizedEmail") 152 | .HasName("EmailIndex"); 153 | 154 | b.HasIndex("NormalizedUserName") 155 | .IsUnique() 156 | .HasName("UserNameIndex") 157 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 158 | 159 | b.ToTable("AspNetUsers"); 160 | }); 161 | 162 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 163 | { 164 | b.Property("Id") 165 | .ValueGeneratedOnAdd() 166 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 167 | 168 | b.Property("ClaimType"); 169 | 170 | b.Property("ClaimValue"); 171 | 172 | b.Property("RoleId"); 173 | 174 | b.HasKey("Id"); 175 | 176 | b.HasIndex("RoleId"); 177 | 178 | b.ToTable("AspNetRoleClaims"); 179 | }); 180 | 181 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 182 | { 183 | b.Property("Id") 184 | .ValueGeneratedOnAdd() 185 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 186 | 187 | b.Property("ClaimType"); 188 | 189 | b.Property("ClaimValue"); 190 | 191 | b.Property("UserId"); 192 | 193 | b.HasKey("Id"); 194 | 195 | b.HasIndex("UserId"); 196 | 197 | b.ToTable("AspNetUserClaims"); 198 | }); 199 | 200 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 201 | { 202 | b.Property("LoginProvider"); 203 | 204 | b.Property("ProviderKey"); 205 | 206 | b.Property("ProviderDisplayName"); 207 | 208 | b.Property("UserId"); 209 | 210 | b.HasKey("LoginProvider", "ProviderKey"); 211 | 212 | b.HasIndex("UserId"); 213 | 214 | b.ToTable("AspNetUserLogins"); 215 | }); 216 | 217 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 218 | { 219 | b.Property("UserId"); 220 | 221 | b.Property("RoleId"); 222 | 223 | b.HasKey("UserId", "RoleId"); 224 | 225 | b.HasIndex("RoleId"); 226 | 227 | b.ToTable("AspNetUserRoles"); 228 | }); 229 | 230 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 231 | { 232 | b.Property("UserId"); 233 | 234 | b.Property("LoginProvider"); 235 | 236 | b.Property("Name"); 237 | 238 | b.Property("Value"); 239 | 240 | b.HasKey("UserId", "LoginProvider", "Name"); 241 | 242 | b.ToTable("AspNetUserTokens"); 243 | }); 244 | 245 | modelBuilder.Entity("Entities.Category", b => 246 | { 247 | b.HasOne("Entities.Category", "ParentCategory") 248 | .WithMany("ChildCategories") 249 | .HasForeignKey("ParentCategoryId"); 250 | }); 251 | 252 | modelBuilder.Entity("Entities.Post", b => 253 | { 254 | b.HasOne("Entities.User", "Author") 255 | .WithMany("Posts") 256 | .HasForeignKey("AuthorId") 257 | .OnDelete(DeleteBehavior.Restrict); 258 | 259 | b.HasOne("Entities.Category", "Category") 260 | .WithMany("Posts") 261 | .HasForeignKey("CategoryId") 262 | .OnDelete(DeleteBehavior.Restrict); 263 | }); 264 | 265 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 266 | { 267 | b.HasOne("Entities.Role") 268 | .WithMany() 269 | .HasForeignKey("RoleId") 270 | .OnDelete(DeleteBehavior.Restrict); 271 | }); 272 | 273 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 274 | { 275 | b.HasOne("Entities.User") 276 | .WithMany() 277 | .HasForeignKey("UserId") 278 | .OnDelete(DeleteBehavior.Restrict); 279 | }); 280 | 281 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 282 | { 283 | b.HasOne("Entities.User") 284 | .WithMany() 285 | .HasForeignKey("UserId") 286 | .OnDelete(DeleteBehavior.Restrict); 287 | }); 288 | 289 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 290 | { 291 | b.HasOne("Entities.Role") 292 | .WithMany() 293 | .HasForeignKey("RoleId") 294 | .OnDelete(DeleteBehavior.Restrict); 295 | 296 | b.HasOne("Entities.User") 297 | .WithMany() 298 | .HasForeignKey("UserId") 299 | .OnDelete(DeleteBehavior.Restrict); 300 | }); 301 | 302 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 303 | { 304 | b.HasOne("Entities.User") 305 | .WithMany() 306 | .HasForeignKey("UserId") 307 | .OnDelete(DeleteBehavior.Restrict); 308 | }); 309 | #pragma warning restore 612, 618 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /Data/Repositories/Repository.cs: -------------------------------------------------------------------------------- 1 | using Common.Utilities; 2 | using Entities; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Data.Repositories 12 | { 13 | public class Repository : IRepository 14 | where TEntity : class, IEntity 15 | { 16 | protected readonly ApplicationDbContext DbContext; 17 | public DbSet Entities { get; } 18 | public virtual IQueryable Table => Entities; 19 | public virtual IQueryable TableNoTracking => Entities.AsNoTracking(); 20 | 21 | public Repository(ApplicationDbContext dbContext) 22 | { 23 | DbContext = dbContext; 24 | Entities = DbContext.Set(); // City => Cities 25 | } 26 | 27 | #region Async Method 28 | public virtual ValueTask GetByIdAsync(CancellationToken cancellationToken, params object[] ids) 29 | { 30 | return Entities.FindAsync(ids, cancellationToken); 31 | } 32 | 33 | public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken, bool saveNow = true) 34 | { 35 | Assert.NotNull(entity, nameof(entity)); 36 | await Entities.AddAsync(entity, cancellationToken).ConfigureAwait(false); 37 | if (saveNow) 38 | await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); 39 | } 40 | 41 | public virtual async Task AddRangeAsync(IEnumerable entities, CancellationToken cancellationToken, bool saveNow = true) 42 | { 43 | Assert.NotNull(entities, nameof(entities)); 44 | await Entities.AddRangeAsync(entities, cancellationToken).ConfigureAwait(false); 45 | if (saveNow) 46 | await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); 47 | } 48 | 49 | public virtual async Task UpdateAsync(TEntity entity, CancellationToken cancellationToken, bool saveNow = true) 50 | { 51 | Assert.NotNull(entity, nameof(entity)); 52 | Entities.Update(entity); 53 | if (saveNow) 54 | await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); 55 | } 56 | 57 | public virtual async Task UpdateRangeAsync(IEnumerable entities, CancellationToken cancellationToken, bool saveNow = true) 58 | { 59 | Assert.NotNull(entities, nameof(entities)); 60 | Entities.UpdateRange(entities); 61 | if (saveNow) 62 | await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); 63 | } 64 | 65 | public virtual async Task DeleteAsync(TEntity entity, CancellationToken cancellationToken, bool saveNow = true) 66 | { 67 | Assert.NotNull(entity, nameof(entity)); 68 | Entities.Remove(entity); 69 | if (saveNow) 70 | await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); 71 | } 72 | 73 | public virtual async Task DeleteRangeAsync(IEnumerable entities, CancellationToken cancellationToken, bool saveNow = true) 74 | { 75 | Assert.NotNull(entities, nameof(entities)); 76 | Entities.RemoveRange(entities); 77 | if (saveNow) 78 | await DbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); 79 | } 80 | #endregion 81 | 82 | #region Sync Methods 83 | public virtual TEntity GetById(params object[] ids) 84 | { 85 | return Entities.Find(ids); 86 | } 87 | 88 | public virtual void Add(TEntity entity, bool saveNow = true) 89 | { 90 | Assert.NotNull(entity, nameof(entity)); 91 | Entities.Add(entity); 92 | if (saveNow) 93 | DbContext.SaveChanges(); 94 | } 95 | 96 | public virtual void AddRange(IEnumerable entities, bool saveNow = true) 97 | { 98 | Assert.NotNull(entities, nameof(entities)); 99 | Entities.AddRange(entities); 100 | if (saveNow) 101 | DbContext.SaveChanges(); 102 | } 103 | 104 | public virtual void Update(TEntity entity, bool saveNow = true) 105 | { 106 | Assert.NotNull(entity, nameof(entity)); 107 | Entities.Update(entity); 108 | if (saveNow) 109 | DbContext.SaveChanges(); 110 | } 111 | 112 | public virtual void UpdateRange(IEnumerable entities, bool saveNow = true) 113 | { 114 | Assert.NotNull(entities, nameof(entities)); 115 | Entities.UpdateRange(entities); 116 | if (saveNow) 117 | DbContext.SaveChanges(); 118 | } 119 | 120 | public virtual void Delete(TEntity entity, bool saveNow = true) 121 | { 122 | Assert.NotNull(entity, nameof(entity)); 123 | Entities.Remove(entity); 124 | if (saveNow) 125 | DbContext.SaveChanges(); 126 | } 127 | 128 | public virtual void DeleteRange(IEnumerable entities, bool saveNow = true) 129 | { 130 | Assert.NotNull(entities, nameof(entities)); 131 | Entities.RemoveRange(entities); 132 | if (saveNow) 133 | DbContext.SaveChanges(); 134 | } 135 | #endregion 136 | 137 | #region Attach & Detach 138 | public virtual void Detach(TEntity entity) 139 | { 140 | Assert.NotNull(entity, nameof(entity)); 141 | var entry = DbContext.Entry(entity); 142 | if (entry != null) 143 | entry.State = EntityState.Detached; 144 | } 145 | 146 | public virtual void Attach(TEntity entity) 147 | { 148 | Assert.NotNull(entity, nameof(entity)); 149 | if (DbContext.Entry(entity).State == EntityState.Detached) 150 | Entities.Attach(entity); 151 | } 152 | #endregion 153 | 154 | #region Explicit Loading 155 | public virtual async Task LoadCollectionAsync(TEntity entity, Expression>> collectionProperty, CancellationToken cancellationToken) 156 | where TProperty : class 157 | { 158 | Attach(entity); 159 | 160 | var collection = DbContext.Entry(entity).Collection(collectionProperty); 161 | if (!collection.IsLoaded) 162 | await collection.LoadAsync(cancellationToken).ConfigureAwait(false); 163 | } 164 | 165 | public virtual void LoadCollection(TEntity entity, Expression>> collectionProperty) 166 | where TProperty : class 167 | { 168 | Attach(entity); 169 | var collection = DbContext.Entry(entity).Collection(collectionProperty); 170 | if (!collection.IsLoaded) 171 | collection.Load(); 172 | } 173 | 174 | public virtual async Task LoadReferenceAsync(TEntity entity, Expression> referenceProperty, CancellationToken cancellationToken) 175 | where TProperty : class 176 | { 177 | Attach(entity); 178 | var reference = DbContext.Entry(entity).Reference(referenceProperty); 179 | if (!reference.IsLoaded) 180 | await reference.LoadAsync(cancellationToken).ConfigureAwait(false); 181 | } 182 | 183 | public virtual void LoadReference(TEntity entity, Expression> referenceProperty) 184 | where TProperty : class 185 | { 186 | Attach(entity); 187 | var reference = DbContext.Entry(entity).Reference(referenceProperty); 188 | if (!reference.IsLoaded) 189 | reference.Load(); 190 | } 191 | #endregion 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /Data/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Common.Exceptions; 3 | using Common.Utilities; 4 | using Entities; 5 | using Microsoft.EntityFrameworkCore; 6 | using System; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace Data.Repositories 12 | { 13 | public class UserRepository : Repository, IUserRepository, IScopedDependency 14 | { 15 | public UserRepository(ApplicationDbContext dbContext) 16 | : base(dbContext) 17 | { 18 | } 19 | 20 | public Task GetByUserAndPass(string username, string password, CancellationToken cancellationToken) 21 | { 22 | var passwordHash = SecurityHelper.GetSha256Hash(password); 23 | return Table.Where(p => p.UserName == username && p.PasswordHash == passwordHash).SingleOrDefaultAsync(cancellationToken); 24 | } 25 | 26 | public Task UpdateSecurityStampAsync(User user, CancellationToken cancellationToken) 27 | { 28 | //user.SecurityStamp = Guid.NewGuid(); 29 | return UpdateAsync(user, cancellationToken); 30 | } 31 | 32 | //public override void Update(User entity, bool saveNow = true) 33 | //{ 34 | // entity.SecurityStamp = Guid.NewGuid(); 35 | // base.Update(entity, saveNow); 36 | //} 37 | 38 | public Task UpdateLastLoginDateAsync(User user, CancellationToken cancellationToken) 39 | { 40 | user.LastLoginDate = DateTimeOffset.Now; 41 | return UpdateAsync(user, cancellationToken); 42 | } 43 | 44 | public async Task AddAsync(User user, string password, CancellationToken cancellationToken) 45 | { 46 | var exists = await TableNoTracking.AnyAsync(p => p.UserName == user.UserName); 47 | if (exists) 48 | throw new BadRequestException("نام کاربری تکراری است"); 49 | 50 | var passwordHash = SecurityHelper.GetSha256Hash(password); 51 | user.PasswordHash = passwordHash; 52 | await base.AddAsync(user, cancellationToken); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ELMAH-SQLServer.sql: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | ELMAH - Error Logging Modules and Handlers for ASP.NET 4 | Copyright (c) 2004-9 Atif Aziz. All rights reserved. 5 | 6 | Author(s): 7 | 8 | Atif Aziz, http://www.raboof.com 9 | Phil Haacked, http://haacked.com 10 | 11 | Licensed under the Apache License, Version 2.0 (the "License"); 12 | you may not use this file except in compliance with the License. 13 | You may obtain a copy of the License at 14 | 15 | http://www.apache.org/licenses/LICENSE-2.0 16 | 17 | Unless required by applicable law or agreed to in writing, software 18 | distributed under the License is distributed on an "AS IS" BASIS, 19 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | See the License for the specific language governing permissions and 21 | limitations under the License. 22 | 23 | */ 24 | 25 | -- ELMAH DDL script for Microsoft SQL Server 2000 or later. 26 | 27 | -- $Id: SQLServer.sql addb64b2f0fa 2012-03-07 18:50:16Z azizatif $ 28 | 29 | DECLARE @DBCompatibilityLevel INT 30 | DECLARE @DBCompatibilityLevelMajor INT 31 | DECLARE @DBCompatibilityLevelMinor INT 32 | 33 | SELECT 34 | @DBCompatibilityLevel = cmptlevel 35 | FROM 36 | master.dbo.sysdatabases 37 | WHERE 38 | name = DB_NAME() 39 | 40 | IF @DBCompatibilityLevel <> 80 41 | BEGIN 42 | 43 | SELECT @DBCompatibilityLevelMajor = @DBCompatibilityLevel / 10, 44 | @DBCompatibilityLevelMinor = @DBCompatibilityLevel % 10 45 | 46 | PRINT N' 47 | =========================================================================== 48 | WARNING! 49 | --------------------------------------------------------------------------- 50 | 51 | This script is designed for Microsoft SQL Server 2000 (8.0) but your 52 | database is set up for compatibility with version ' 53 | + CAST(@DBCompatibilityLevelMajor AS NVARCHAR(80)) 54 | + N'.' 55 | + CAST(@DBCompatibilityLevelMinor AS NVARCHAR(80)) 56 | + N'. Although 57 | the script should work with later versions of Microsoft SQL Server, 58 | you can ensure compatibility by executing the following statement: 59 | 60 | ALTER DATABASE [' 61 | + DB_NAME() 62 | + N'] 63 | SET COMPATIBILITY_LEVEL = 80 64 | 65 | If you are hosting ELMAH in the same database as your application 66 | database and do not wish to change the compatibility option then you 67 | should create a separate database to host ELMAH where you can set the 68 | compatibility level more freely. 69 | 70 | If you continue with the current setup, please report any compatibility 71 | issues you encounter over at: 72 | 73 | http://code.google.com/p/elmah/issues/list 74 | 75 | =========================================================================== 76 | ' 77 | END 78 | GO 79 | 80 | /* ------------------------------------------------------------------------ 81 | TABLES 82 | ------------------------------------------------------------------------ */ 83 | 84 | CREATE TABLE [dbo].[ELMAH_Error] 85 | ( 86 | [ErrorId] UNIQUEIDENTIFIER NOT NULL, 87 | [Application] NVARCHAR(60) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, 88 | [Host] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, 89 | [Type] NVARCHAR(100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, 90 | [Source] NVARCHAR(60) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, 91 | [Message] NVARCHAR(500) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, 92 | [User] NVARCHAR(50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL, 93 | [StatusCode] INT NOT NULL, 94 | [TimeUtc] DATETIME NOT NULL, 95 | [Sequence] INT IDENTITY (1, 1) NOT NULL, 96 | [AllXml] NTEXT COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL 97 | ) 98 | ON [PRIMARY] TEXTIMAGE_ON [PRIMARY] 99 | 100 | GO 101 | 102 | ALTER TABLE [dbo].[ELMAH_Error] WITH NOCHECK ADD 103 | CONSTRAINT [PK_ELMAH_Error] PRIMARY KEY NONCLUSTERED ([ErrorId]) ON [PRIMARY] 104 | GO 105 | 106 | ALTER TABLE [dbo].[ELMAH_Error] ADD 107 | CONSTRAINT [DF_ELMAH_Error_ErrorId] DEFAULT (NEWID()) FOR [ErrorId] 108 | GO 109 | 110 | CREATE NONCLUSTERED INDEX [IX_ELMAH_Error_App_Time_Seq] ON [dbo].[ELMAH_Error] 111 | ( 112 | [Application] ASC, 113 | [TimeUtc] DESC, 114 | [Sequence] DESC 115 | ) 116 | ON [PRIMARY] 117 | GO 118 | 119 | /* ------------------------------------------------------------------------ 120 | STORED PROCEDURES 121 | ------------------------------------------------------------------------ */ 122 | 123 | SET QUOTED_IDENTIFIER ON 124 | GO 125 | SET ANSI_NULLS ON 126 | GO 127 | 128 | CREATE PROCEDURE [dbo].[ELMAH_GetErrorXml] 129 | ( 130 | @Application NVARCHAR(60), 131 | @ErrorId UNIQUEIDENTIFIER 132 | ) 133 | AS 134 | 135 | SET NOCOUNT ON 136 | 137 | SELECT 138 | [AllXml] 139 | FROM 140 | [ELMAH_Error] 141 | WHERE 142 | [ErrorId] = @ErrorId 143 | AND 144 | [Application] = @Application 145 | 146 | GO 147 | SET QUOTED_IDENTIFIER OFF 148 | GO 149 | SET ANSI_NULLS ON 150 | GO 151 | 152 | SET QUOTED_IDENTIFIER ON 153 | GO 154 | SET ANSI_NULLS ON 155 | GO 156 | 157 | CREATE PROCEDURE [dbo].[ELMAH_GetErrorsXml] 158 | ( 159 | @Application NVARCHAR(60), 160 | @PageIndex INT = 0, 161 | @PageSize INT = 15, 162 | @TotalCount INT OUTPUT 163 | ) 164 | AS 165 | 166 | SET NOCOUNT ON 167 | 168 | DECLARE @FirstTimeUTC DATETIME 169 | DECLARE @FirstSequence INT 170 | DECLARE @StartRow INT 171 | DECLARE @StartRowIndex INT 172 | 173 | SELECT 174 | @TotalCount = COUNT(1) 175 | FROM 176 | [ELMAH_Error] 177 | WHERE 178 | [Application] = @Application 179 | 180 | -- Get the ID of the first error for the requested page 181 | 182 | SET @StartRowIndex = @PageIndex * @PageSize + 1 183 | 184 | IF @StartRowIndex <= @TotalCount 185 | BEGIN 186 | 187 | SET ROWCOUNT @StartRowIndex 188 | 189 | SELECT 190 | @FirstTimeUTC = [TimeUtc], 191 | @FirstSequence = [Sequence] 192 | FROM 193 | [ELMAH_Error] 194 | WHERE 195 | [Application] = @Application 196 | ORDER BY 197 | [TimeUtc] DESC, 198 | [Sequence] DESC 199 | 200 | END 201 | ELSE 202 | BEGIN 203 | 204 | SET @PageSize = 0 205 | 206 | END 207 | 208 | -- Now set the row count to the requested page size and get 209 | -- all records below it for the pertaining application. 210 | 211 | SET ROWCOUNT @PageSize 212 | 213 | SELECT 214 | errorId = [ErrorId], 215 | application = [Application], 216 | host = [Host], 217 | type = [Type], 218 | source = [Source], 219 | message = [Message], 220 | [user] = [User], 221 | statusCode = [StatusCode], 222 | time = CONVERT(VARCHAR(50), [TimeUtc], 126) + 'Z' 223 | FROM 224 | [ELMAH_Error] error 225 | WHERE 226 | [Application] = @Application 227 | AND 228 | [TimeUtc] <= @FirstTimeUTC 229 | AND 230 | [Sequence] <= @FirstSequence 231 | ORDER BY 232 | [TimeUtc] DESC, 233 | [Sequence] DESC 234 | FOR 235 | XML AUTO 236 | 237 | GO 238 | SET QUOTED_IDENTIFIER OFF 239 | GO 240 | SET ANSI_NULLS ON 241 | GO 242 | 243 | SET QUOTED_IDENTIFIER ON 244 | GO 245 | SET ANSI_NULLS ON 246 | GO 247 | 248 | CREATE PROCEDURE [dbo].[ELMAH_LogError] 249 | ( 250 | @ErrorId UNIQUEIDENTIFIER, 251 | @Application NVARCHAR(60), 252 | @Host NVARCHAR(30), 253 | @Type NVARCHAR(100), 254 | @Source NVARCHAR(60), 255 | @Message NVARCHAR(500), 256 | @User NVARCHAR(50), 257 | @AllXml NTEXT, 258 | @StatusCode INT, 259 | @TimeUtc DATETIME 260 | ) 261 | AS 262 | 263 | SET NOCOUNT ON 264 | 265 | INSERT 266 | INTO 267 | [ELMAH_Error] 268 | ( 269 | [ErrorId], 270 | [Application], 271 | [Host], 272 | [Type], 273 | [Source], 274 | [Message], 275 | [User], 276 | [AllXml], 277 | [StatusCode], 278 | [TimeUtc] 279 | ) 280 | VALUES 281 | ( 282 | @ErrorId, 283 | @Application, 284 | @Host, 285 | @Type, 286 | @Source, 287 | @Message, 288 | @User, 289 | @AllXml, 290 | @StatusCode, 291 | @TimeUtc 292 | ) 293 | 294 | GO 295 | SET QUOTED_IDENTIFIER OFF 296 | GO 297 | SET ANSI_NULLS ON 298 | GO 299 | 300 | -------------------------------------------------------------------------------- /Entities/Common/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Entities 2 | { 3 | public interface IEntity 4 | { 5 | } 6 | 7 | public interface IEntity : IEntity 8 | { 9 | TKey Id { get; set; } 10 | } 11 | 12 | public abstract class BaseEntity : IEntity 13 | { 14 | public TKey Id { get; set; } 15 | } 16 | 17 | public abstract class BaseEntity : BaseEntity 18 | { 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Entities/Entities.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Entities/Post/Category.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.ComponentModel.DataAnnotations.Schema; 4 | 5 | namespace Entities 6 | { 7 | public class Category : BaseEntity 8 | { 9 | [Required] 10 | [StringLength(50)] 11 | public string Name { get; set; } 12 | public int? ParentCategoryId { get; set; } 13 | 14 | [ForeignKey(nameof(ParentCategoryId))] 15 | public Category ParentCategory { get; set; } 16 | public ICollection ChildCategories { get; set; } 17 | public ICollection Posts { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Entities/Post/Post.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using System; 4 | 5 | namespace Entities 6 | { 7 | public class Post : BaseEntity 8 | { 9 | public string Title { get; set; } 10 | public string Description { get; set; } 11 | public int CategoryId { get; set; } 12 | public int AuthorId { get; set; } 13 | 14 | public Category Category { get; set; } 15 | public User Author { get; set; } 16 | } 17 | 18 | public class PostConfiguration : IEntityTypeConfiguration 19 | { 20 | public void Configure(EntityTypeBuilder builder) 21 | { 22 | builder.Property(p => p.Title).IsRequired().HasMaxLength(200); 23 | builder.Property(p => p.Description).IsRequired(); 24 | builder.HasOne(p => p.Category).WithMany(c => c.Posts).HasForeignKey(p => p.CategoryId); 25 | builder.HasOne(p => p.Author).WithMany(c => c.Posts).HasForeignKey(p => p.AuthorId); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Entities/User/Role.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | namespace Entities 7 | { 8 | public class Role : IdentityRole, IEntity 9 | { 10 | [Required] 11 | [StringLength(100)] 12 | public string Description { get; set; } 13 | } 14 | 15 | public class RoleConfiguration : IEntityTypeConfiguration 16 | { 17 | public void Configure(EntityTypeBuilder builder) 18 | { 19 | builder.Property(p => p.Name).IsRequired().HasMaxLength(50); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Entities/User/User.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.ComponentModel.DataAnnotations; 7 | 8 | namespace Entities 9 | { 10 | public class User : IdentityUser, IEntity 11 | { 12 | public User() 13 | { 14 | IsActive = true; 15 | } 16 | 17 | [Required] 18 | [StringLength(100)] 19 | public string FullName { get; set; } 20 | public int Age { get; set; } 21 | public GenderType Gender { get; set; } 22 | public bool IsActive { get; set; } 23 | public DateTimeOffset? LastLoginDate { get; set; } 24 | 25 | public ICollection Posts { get; set; } 26 | } 27 | 28 | public class UserConfiguration : IEntityTypeConfiguration 29 | { 30 | public void Configure(EntityTypeBuilder builder) 31 | { 32 | builder.Property(p => p.UserName).IsRequired().HasMaxLength(100); 33 | } 34 | } 35 | 36 | public enum GenderType 37 | { 38 | [Display(Name = "مرد")] 39 | Male = 1, 40 | 41 | [Display(Name = "زن")] 42 | Female = 2 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /MyApi.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.28010.2050 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyApi", "MyApi\MyApi.csproj", "{15CBFB10-7335-486D-ADC5-F3D97FC780F4}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{924F7EA2-0F9C-4F6E-8595-1564F5B7B628}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Entities", "Entities\Entities.csproj", "{A2F5D01C-0073-45A3-8EBF-F626A0CDC56A}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Data", "Data\Data.csproj", "{034B9977-3261-4A11-AEA5-EEBEAEAAC31B}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Services", "Services\Services.csproj", "{21F0CCD3-227D-41DD-9FAE-F13EED9CE509}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebFramework", "WebFramework\WebFramework.csproj", "{231CC001-E10C-4BE1-AB77-17130DDDDFB8}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {15CBFB10-7335-486D-ADC5-F3D97FC780F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {15CBFB10-7335-486D-ADC5-F3D97FC780F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {15CBFB10-7335-486D-ADC5-F3D97FC780F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {15CBFB10-7335-486D-ADC5-F3D97FC780F4}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {924F7EA2-0F9C-4F6E-8595-1564F5B7B628}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {924F7EA2-0F9C-4F6E-8595-1564F5B7B628}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {924F7EA2-0F9C-4F6E-8595-1564F5B7B628}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {924F7EA2-0F9C-4F6E-8595-1564F5B7B628}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {A2F5D01C-0073-45A3-8EBF-F626A0CDC56A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {A2F5D01C-0073-45A3-8EBF-F626A0CDC56A}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {A2F5D01C-0073-45A3-8EBF-F626A0CDC56A}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {A2F5D01C-0073-45A3-8EBF-F626A0CDC56A}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {034B9977-3261-4A11-AEA5-EEBEAEAAC31B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {034B9977-3261-4A11-AEA5-EEBEAEAAC31B}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {034B9977-3261-4A11-AEA5-EEBEAEAAC31B}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {034B9977-3261-4A11-AEA5-EEBEAEAAC31B}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {21F0CCD3-227D-41DD-9FAE-F13EED9CE509}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {21F0CCD3-227D-41DD-9FAE-F13EED9CE509}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {21F0CCD3-227D-41DD-9FAE-F13EED9CE509}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {21F0CCD3-227D-41DD-9FAE-F13EED9CE509}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {231CC001-E10C-4BE1-AB77-17130DDDDFB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {231CC001-E10C-4BE1-AB77-17130DDDDFB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {231CC001-E10C-4BE1-AB77-17130DDDDFB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {231CC001-E10C-4BE1-AB77-17130DDDDFB8}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {7FE0111B-D1A5-40E9-AA5A-344E5B5ED548} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /MyApi/Controllers/v1/CategoriesController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Data.Repositories; 3 | using Entities; 4 | using MyApi.Models; 5 | using WebFramework.Api; 6 | 7 | namespace MyApi.Controllers.v1 8 | { 9 | public class CategoriesController : CrudController 10 | { 11 | public CategoriesController(IRepository repository, IMapper mapper) 12 | : base(repository, mapper) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /MyApi/Controllers/v1/OldPostsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using AutoMapper.QueryableExtensions; 3 | using Data.Repositories; 4 | using Entities; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.EntityFrameworkCore; 7 | using MyApi.Models; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using WebFramework.Api; 13 | 14 | namespace MyApi.Controllers.v1 15 | { 16 | [ApiVersion("1")] 17 | public class OldPostsController : BaseController 18 | { 19 | private readonly IRepository _repository; 20 | private readonly IMapper _mapper; 21 | 22 | public OldPostsController(IRepository repository, IMapper mapper) 23 | { 24 | _repository = repository; 25 | _mapper = mapper; 26 | } 27 | 28 | [HttpGet] 29 | public async Task>> Get(CancellationToken cancellationToken) 30 | { 31 | #region old code 32 | //var posts = await _repository.TableNoTracking 33 | // .Include(p => p.Category).Include(p => p.Author).ToListAsync(cancellationToken); 34 | //var list = posts.Select(p => 35 | //{ 36 | // var dto = Mapper.Map(p); 37 | // return dto; 38 | //}).ToList(); 39 | 40 | //var list = await _repository.TableNoTracking.Select(p => new PostDto 41 | //{ 42 | // Id = p.Id, 43 | // Title = p.Title, 44 | // Description = p.Description, 45 | // CategoryId = p.CategoryId, 46 | // AuthorId = p.AuthorId, 47 | // AuthorFullName = p.Author.FullName, 48 | // CategoryName = p.Category.Name 49 | //}).ToListAsync(cancellationToken); 50 | #endregion 51 | 52 | var list = await _repository.TableNoTracking.ProjectTo(_mapper.ConfigurationProvider) 53 | //.Where(postDto => postDto.Title.Contains("test") || postDto.CategoryName.Contains("test")) 54 | .ToListAsync(cancellationToken); 55 | 56 | return Ok(list); 57 | } 58 | 59 | [HttpGet("{id:guid}")] 60 | public async Task> Get(Guid id, CancellationToken cancellationToken) 61 | { 62 | var dto = await _repository.TableNoTracking.ProjectTo(_mapper.ConfigurationProvider) 63 | .SingleOrDefaultAsync(p => p.Id == id, cancellationToken); 64 | 65 | //Post post = null; //Find from database by Id (include) 66 | //var resultDto = PostDto.FromEntity(post); 67 | 68 | if (dto == null) 69 | return NotFound(); 70 | 71 | //dto.Category = "My custom value, not from mapping!"; 72 | 73 | #region old code 74 | //var dto = new PostDto 75 | //{ 76 | // Id = model.Id, 77 | // Title = model.Title, 78 | // Description = model.Description, 79 | // CategoryId = model.CategoryId, 80 | // AuthorId = model.AuthorId, 81 | // AuthorFullName = model.Author.FullName, 82 | // CategoryName = model.Category.Name 83 | //}; 84 | #endregion 85 | 86 | return dto; 87 | } 88 | 89 | [HttpPost] 90 | public async Task> Create(PostDto dto, CancellationToken cancellationToken) 91 | { 92 | //var model = Mapper.Map(dto); 93 | var model = dto.ToEntity(_mapper); 94 | 95 | #region old code 96 | //var model = new Post 97 | //{ 98 | // Title = dto.Title, 99 | // Description = dto.Description, 100 | // CategoryId = dto.CategoryId, 101 | // AuthorId = dto.AuthorId 102 | //}; 103 | #endregion 104 | 105 | await _repository.AddAsync(model, cancellationToken); 106 | 107 | #region old code 108 | //await _repository.LoadReferenceAsync(model, p => p.Category, cancellationToken); 109 | //await _repository.LoadReferenceAsync(model, p => p.Author, cancellationToken); 110 | //model = await _repository.TableNoTracking 111 | // .Include(p => p.Category) 112 | // .Include(p =>p.Author) 113 | // .SingleOrDefaultAsync(p => p.Id == model.Id, cancellationToken); 114 | //var resultDto = new PostDto 115 | //{ 116 | // Id = model.Id, 117 | // Title = model.Title, 118 | // Description = model.Description, 119 | // CategoryId = model.CategoryId, 120 | // AuthorId = model.AuthorId, 121 | // AuthorName = model.Author.FullName, 122 | // CategoryName = model.Category.Name 123 | //}; 124 | 125 | 126 | //var resultDto = await _repository.TableNoTracking.Select(p => new PostDto 127 | //{ 128 | // Id = p.Id, 129 | // Title = p.Title, 130 | // Description = p.Description, 131 | // CategoryId = p.CategoryId, 132 | // AuthorId = p.AuthorId, 133 | // AuthorFullName = p.Author.FullName, 134 | // CategoryName = p.Category.Name 135 | //}).SingleOrDefaultAsync(p => p.Id == model.Id, cancellationToken); 136 | #endregion 137 | 138 | var resultDto = await _repository.TableNoTracking.ProjectTo(_mapper.ConfigurationProvider) 139 | .SingleOrDefaultAsync(p => p.Id == model.Id, cancellationToken); 140 | 141 | return resultDto; 142 | } 143 | 144 | [HttpPut] 145 | public async Task> Update(Guid id, PostDto dto, CancellationToken cancellationToken) 146 | { 147 | //var postDto = new PostDto(); 148 | //Create 149 | //var post = postDto.ToEntity(); // DTO => Entity 150 | //Update 151 | //var updatePost = postDto.ToEntity(post); // DTO => Entity (an exist) 152 | //GetById 153 | //var postDto = PostDto.FromEntity(model); // Entity => DTO 154 | 155 | 156 | var model = await _repository.GetByIdAsync(cancellationToken, id); 157 | 158 | //Mapper.Map(dto, model); 159 | model = dto.ToEntity(_mapper, model); 160 | 161 | #region old code 162 | //model.Title = dto.Title; 163 | //model.Description = dto.Description; 164 | //model.CategoryId = dto.CategoryId; 165 | //model.AuthorId = dto.AuthorId; 166 | #endregion 167 | 168 | await _repository.UpdateAsync(model, cancellationToken); 169 | 170 | #region old code 171 | //var resultDto = await _repository.TableNoTracking.Select(p => new PostDto 172 | //{ 173 | // Id = p.Id, 174 | // Title = p.Title, 175 | // Description = p.Description, 176 | // CategoryId = p.CategoryId, 177 | // AuthorId = p.AuthorId, 178 | // AuthorFullName = p.Author.FullName, 179 | // CategoryName = p.Category.Name 180 | //}).SingleOrDefaultAsync(p => p.Id == model.Id, cancellationToken); 181 | #endregion 182 | 183 | var resultDto = await _repository.TableNoTracking.ProjectTo(_mapper.ConfigurationProvider) 184 | .SingleOrDefaultAsync(p => p.Id == model.Id, cancellationToken); 185 | 186 | return resultDto; 187 | } 188 | 189 | [HttpDelete("{id:guid}")] 190 | public async Task Delete(Guid id, CancellationToken cancellationToken) 191 | { 192 | var model = await _repository.GetByIdAsync(cancellationToken, id); 193 | await _repository.DeleteAsync(model, cancellationToken); 194 | 195 | return Ok(); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /MyApi/Controllers/v1/PostsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Data.Repositories; 3 | using Entities; 4 | using MyApi.Models; 5 | using System; 6 | using WebFramework.Api; 7 | 8 | namespace MyApi.Controllers.v1 9 | { 10 | public class PostsController : CrudController 11 | { 12 | public PostsController(IRepository repository, IMapper mapper) 13 | : base(repository, mapper) 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MyApi/Controllers/v1/TestController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using MyApi.Models; 5 | using Newtonsoft.Json; 6 | using Swashbuckle.AspNetCore.Annotations; 7 | using Swashbuckle.AspNetCore.Filters; 8 | using System.ComponentModel; 9 | using System.ComponentModel.DataAnnotations; 10 | using WebFramework.Api; 11 | 12 | namespace MyApi.Controllers.v1 13 | { 14 | #region Swagger Annotations 15 | public class AddressDto 16 | { 17 | /// 18 | /// 3-letter ISO country code 19 | /// 20 | /// Iran 21 | [Required] 22 | public string Country { get; set; } 23 | 24 | /// 25 | /// Name of city 26 | /// 27 | /// Seattle 28 | [DefaultValue("Seattle")] 29 | public string City { get; set; } 30 | 31 | [JsonProperty("promo-code")] 32 | public string Code { get; set; } 33 | 34 | [JsonIgnore] 35 | public int Discount { get; set; } 36 | } 37 | 38 | ///// 39 | ///// Retrieves a specific product by unique id 40 | ///// 41 | ///// Parameter 1 description 42 | ///// Parameter 2 description 43 | ///// Parameter 2 description 44 | ///// Awesomeness! 45 | ///// Product created 46 | ///// Product has missing/invalid values 47 | ///// Oops! Can't create your product right now 48 | //[HttpGet("Test")] 49 | //public ActionResult Test(/* 50 | // IFormFile file, ... 51 | // [FromQuery] Address address, ... 52 | // [FromForm] parameter, ... 53 | // [FromBody] parameter, ... 54 | // [Required] parameter, ... */) 55 | //{ 56 | // throw new NotImplementedException(); 57 | //} 58 | #endregion 59 | 60 | [ApiVersion("1")] 61 | [AllowAnonymous] 62 | public class TestController : BaseController 63 | { 64 | [HttpPost("[action]")] 65 | public ActionResult UploadFile1(IFormFile file1) 66 | { 67 | return Ok(); 68 | } 69 | 70 | //It doesn't work anymore in recent versions because of replacing Swashbuckle.AspNetCore.Examples with Swashbuckle.AspNetCore.Filters 71 | //[AddSwaggerFileUploadButton] 72 | [HttpPost("[action]")] 73 | public ActionResult UploadFile2() 74 | { 75 | var file = Request.Form.Files[0]; 76 | return Ok(); 77 | } 78 | 79 | 80 | #region Action Annotations 81 | //Specific request content type 82 | //[Consumes("application/json")] 83 | //Specific response content type 84 | //[Produces("application/json")] 85 | 86 | //Specific response http status codes 87 | //[ProducesResponseType(200)] 88 | //[ProducesResponseType(StatusCodes.Status200OK)] 89 | //[SwaggerResponse(200)] 90 | //[SwaggerResponse(StatusCodes.Status200OK)] 91 | 92 | //Specific response type & description 93 | //[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] 94 | //[SwaggerResponse(StatusCodes.Status200OK, type: typeof(IEnumerable))] 95 | //[SwaggerResponse(StatusCodes.Status200OK, "my custom descriptions", type: typeof(IEnumerable))] 96 | 97 | //[SwaggerOperation(OperationId = "CreateCart")] 98 | //[SwaggerOperation(OperationId = "DeleteCart", Summary = "Deletes a specific cart", Description = "Requires admin privileges")] 99 | //[SwaggerOperationFilter(typeof(MyCustomIOperationFilter))] 100 | //[SwaggerTag("Manipulate Carts to your heart's content", "http://www.tempuri.org")] 101 | #endregion 102 | 103 | [HttpPost("[action]")] 104 | [SwaggerRequestExample(typeof(UserDto), typeof(CreateUserRequestExample))] 105 | [SwaggerResponseExample(200, typeof(CreateUserResponseExample))] 106 | [SwaggerResponse(200)] 107 | [SwaggerResponse(StatusCodes.Status406NotAcceptable)] 108 | public ActionResult CreateUser(UserDto userDto) 109 | { 110 | return Ok(userDto); 111 | } 112 | 113 | ///// 114 | ///// Asign an address to user 115 | ///// 116 | ///// Address of user 117 | ///// Awesomeness! 118 | ///// Address added 119 | ///// Address has missing/invalid values 120 | ///// Oops! Can't create your Address right now 121 | [HttpPost("[action]")] 122 | [Consumes("application/json")] 123 | [Produces("application/json")] 124 | [ProducesResponseType(200)] 125 | public ActionResult Address(AddressDto addressDto) 126 | { 127 | return Ok(); 128 | } 129 | } 130 | } 131 | 132 | public class CreateUserRequestExample : IExamplesProvider 133 | { 134 | public UserDto GetExamples() 135 | { 136 | return new UserDto 137 | { 138 | FullName = "محمدجواد ابراهیمی", 139 | Age = 25, 140 | UserName = "mjebrahimi", 141 | Email = "admin@site.com", 142 | Gender = Entities.GenderType.Male, 143 | Password = "1234567" 144 | }; 145 | } 146 | } 147 | 148 | public class CreateUserResponseExample : IExamplesProvider 149 | { 150 | public UserDto GetExamples() 151 | { 152 | return new UserDto 153 | { 154 | FullName = "محمدجواد ابراهیمی", 155 | Age = 25, 156 | UserName = "mjebrahimi", 157 | Email = "admin@site.com", 158 | Gender = Entities.GenderType.Male, 159 | Password = "1234567" 160 | }; 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /MyApi/Controllers/v1/UsersController.cs: -------------------------------------------------------------------------------- 1 | using Common.Exceptions; 2 | using Data.Repositories; 3 | using ElmahCore; 4 | using Entities; 5 | using Microsoft.AspNetCore.Authorization; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.EntityFrameworkCore; 9 | using Microsoft.Extensions.Logging; 10 | using MyApi.Models; 11 | using Services; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Threading; 15 | using System.Threading.Tasks; 16 | using WebFramework.Api; 17 | using Microsoft.AspNetCore.Identity; 18 | 19 | namespace MyApi.Controllers.v1 20 | { 21 | [ApiVersion("1")] 22 | public class UsersController : BaseController 23 | { 24 | private readonly IUserRepository userRepository; 25 | private readonly ILogger logger; 26 | private readonly IJwtService jwtService; 27 | private readonly UserManager userManager; 28 | private readonly RoleManager roleManager; 29 | private readonly SignInManager signInManager; 30 | 31 | public UsersController(IUserRepository userRepository, ILogger logger, IJwtService jwtService, 32 | UserManager userManager, RoleManager roleManager, SignInManager signInManager) 33 | { 34 | this.userRepository = userRepository; 35 | this.logger = logger; 36 | this.jwtService = jwtService; 37 | this.userManager = userManager; 38 | this.roleManager = roleManager; 39 | this.signInManager = signInManager; 40 | } 41 | 42 | [HttpGet] 43 | [Authorize(Roles = "Admin")] 44 | public virtual async Task>> Get(CancellationToken cancellationToken) 45 | { 46 | //var userName = HttpContext.User.Identity.GetUserName(); 47 | //userName = HttpContext.User.Identity.Name; 48 | //var userId = HttpContext.User.Identity.GetUserId(); 49 | //var userIdInt = HttpContext.User.Identity.GetUserId(); 50 | //var phone = HttpContext.User.Identity.FindFirstValue(ClaimTypes.MobilePhone); 51 | //var role = HttpContext.User.Identity.FindFirstValue(ClaimTypes.Role); 52 | 53 | var users = await userRepository.TableNoTracking.ToListAsync(cancellationToken); 54 | return Ok(users); 55 | } 56 | 57 | [HttpGet("{id:int}")] 58 | public virtual async Task> Get(int id, CancellationToken cancellationToken) 59 | { 60 | var user2 = await userManager.FindByIdAsync(id.ToString()); 61 | var role = await roleManager.FindByNameAsync("Admin"); 62 | 63 | var user = await userRepository.GetByIdAsync(cancellationToken, id); 64 | if (user == null) 65 | return NotFound(); 66 | 67 | await userManager.UpdateSecurityStampAsync(user); 68 | //await userRepository.UpdateSecurityStampAsync(user, cancellationToken); 69 | 70 | return user; 71 | } 72 | 73 | /// 74 | /// This method generate JWT Token 75 | /// 76 | /// The information of token request 77 | /// 78 | /// 79 | [HttpPost("[action]")] 80 | [AllowAnonymous] 81 | public virtual async Task Token([FromForm] TokenRequest tokenRequest, CancellationToken cancellationToken) 82 | { 83 | if (!tokenRequest.grant_type.Equals("password", StringComparison.OrdinalIgnoreCase)) 84 | throw new Exception("OAuth flow is not password."); 85 | 86 | //var user = await userRepository.GetByUserAndPass(username, password, cancellationToken); 87 | var user = await userManager.FindByNameAsync(tokenRequest.username); 88 | if (user == null) 89 | throw new BadRequestException("نام کاربری یا رمز عبور اشتباه است"); 90 | 91 | var isPasswordValid = await userManager.CheckPasswordAsync(user, tokenRequest.password); 92 | if (!isPasswordValid) 93 | throw new BadRequestException("نام کاربری یا رمز عبور اشتباه است"); 94 | 95 | 96 | //if (user == null) 97 | // throw new BadRequestException("نام کاربری یا رمز عبور اشتباه است"); 98 | 99 | var jwt = await jwtService.GenerateAsync(user); 100 | return new JsonResult(jwt); 101 | } 102 | 103 | [HttpPost] 104 | [AllowAnonymous] 105 | public virtual async Task> Create(UserDto userDto, CancellationToken cancellationToken) 106 | { 107 | logger.LogError("متد Create فراخوانی شد"); 108 | HttpContext.RiseError(new Exception("متد Create فراخوانی شد")); 109 | 110 | //var exists = await userRepository.TableNoTracking.AnyAsync(p => p.UserName == userDto.UserName); 111 | //if (exists) 112 | // return BadRequest("نام کاربری تکراری است"); 113 | 114 | 115 | var user = new User 116 | { 117 | Age = userDto.Age, 118 | FullName = userDto.FullName, 119 | Gender = userDto.Gender, 120 | UserName = userDto.UserName, 121 | Email = userDto.Email 122 | }; 123 | var result = await userManager.CreateAsync(user, userDto.Password); 124 | 125 | var result2 = await roleManager.CreateAsync(new Role 126 | { 127 | Name = "Admin", 128 | Description = "admin role" 129 | }); 130 | 131 | var result3 = await userManager.AddToRoleAsync(user, "Admin"); 132 | 133 | //await userRepository.AddAsync(user, userDto.Password, cancellationToken); 134 | return user; 135 | } 136 | 137 | [HttpPut] 138 | public virtual async Task Update(int id, User user, CancellationToken cancellationToken) 139 | { 140 | var updateUser = await userRepository.GetByIdAsync(cancellationToken, id); 141 | 142 | updateUser.UserName = user.UserName; 143 | updateUser.PasswordHash = user.PasswordHash; 144 | updateUser.FullName = user.FullName; 145 | updateUser.Age = user.Age; 146 | updateUser.Gender = user.Gender; 147 | updateUser.IsActive = user.IsActive; 148 | updateUser.LastLoginDate = user.LastLoginDate; 149 | 150 | await userRepository.UpdateAsync(updateUser, cancellationToken); 151 | 152 | return Ok(); 153 | } 154 | 155 | [HttpDelete] 156 | public virtual async Task Delete(int id, CancellationToken cancellationToken) 157 | { 158 | var user = await userRepository.GetByIdAsync(cancellationToken, id); 159 | await userRepository.DeleteAsync(user, cancellationToken); 160 | 161 | return Ok(); 162 | } 163 | 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /MyApi/Controllers/v2/PostsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Data.Repositories; 3 | using Entities; 4 | using Microsoft.AspNetCore.Mvc; 5 | using MyApi.Models; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using WebFramework.Api; 11 | 12 | namespace MyApi.Controllers.v2 13 | { 14 | [ApiVersion("2")] 15 | public class PostsController : v1.PostsController 16 | { 17 | public PostsController(IRepository repository, IMapper mapper) 18 | : base(repository, mapper) 19 | { 20 | } 21 | 22 | public override Task> Create(PostDto dto, CancellationToken cancellationToken) 23 | { 24 | return base.Create(dto, cancellationToken); 25 | } 26 | 27 | [NonAction] 28 | public override Task Delete(Guid id, CancellationToken cancellationToken) 29 | { 30 | return base.Delete(id, cancellationToken); 31 | } 32 | 33 | public async override Task>> Get(CancellationToken cancellationToken) 34 | { 35 | return await Task.FromResult(new List 36 | { 37 | new PostSelectDto 38 | { 39 | FullTitle = "FullTitle", 40 | AuthorFullName = "AuthorFullName", 41 | CategoryName = "CategoryName", 42 | Description = "Description", 43 | Title = "Title", 44 | } 45 | }); 46 | } 47 | 48 | public async override Task> Get(Guid id, CancellationToken cancellationToken) 49 | { 50 | if (Guid.Empty == id) 51 | return NotFound(); 52 | return await base.Get(id, cancellationToken); 53 | } 54 | 55 | [HttpGet("Test")] 56 | public ActionResult Test() 57 | { 58 | return Content("This is test"); 59 | } 60 | 61 | public override Task> Update(Guid id, PostDto dto, CancellationToken cancellationToken) 62 | { 63 | return base.Update(id, dto, cancellationToken); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /MyApi/Controllers/v2/UsersController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Data.Repositories; 5 | using Entities; 6 | using Microsoft.AspNetCore.Identity; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.Logging; 9 | using MyApi.Models; 10 | using Services; 11 | using WebFramework.Api; 12 | 13 | namespace MyApi.Controllers.v2 14 | { 15 | [ApiVersion("2")] 16 | public class UsersController : v1.UsersController 17 | { 18 | public UsersController(IUserRepository userRepository, 19 | ILogger logger, 20 | IJwtService jwtService, 21 | UserManager userManager, 22 | RoleManager roleManager, 23 | SignInManager signInManager) 24 | : base(userRepository, logger, jwtService, userManager, roleManager, signInManager) 25 | { 26 | } 27 | 28 | public override Task> Create(UserDto userDto, CancellationToken cancellationToken) 29 | { 30 | return base.Create(userDto, cancellationToken); 31 | } 32 | 33 | public override Task Delete(int id, CancellationToken cancellationToken) 34 | { 35 | return base.Delete(id, cancellationToken); 36 | } 37 | 38 | public override Task>> Get(CancellationToken cancellationToken) 39 | { 40 | return base.Get(cancellationToken); 41 | } 42 | 43 | public override Task> Get(int id, CancellationToken cancellationToken) 44 | { 45 | return base.Get(id, cancellationToken); 46 | } 47 | 48 | public override Task Token([FromForm] TokenRequest tokenRequest, CancellationToken cancellationToken) 49 | { 50 | return base.Token(tokenRequest, cancellationToken); 51 | } 52 | 53 | public override Task Update(int id, User user, CancellationToken cancellationToken) 54 | { 55 | return base.Update(id, user, cancellationToken); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /MyApi/Models/CategoryDto.cs: -------------------------------------------------------------------------------- 1 | using Entities; 2 | using WebFramework.Api; 3 | 4 | namespace MyApi.Models 5 | { 6 | public class CategoryDto : BaseDto 7 | { 8 | public string Name { get; set; } 9 | public int? ParentCategoryId { get; set; } 10 | 11 | public string ParentCategoryName { get; set; } //=> mapped from ParentCategory.Name 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /MyApi/Models/PostCustomMapping.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Collections.Generic; 3 | //using AutoMapper; 4 | //using Entities; 5 | //using WebFramework.CustomMapping; 6 | 7 | //namespace MyApi.Models 8 | //{ 9 | // public class PostCustomMapping : IHaveCustomMapping 10 | // { 11 | // public void CreateMappings(Profile profile) 12 | // { 13 | // profile.CreateMap().ReverseMap() 14 | // .ForMember(p => p.Author, opt => opt.Ignore()) 15 | // .ForMember(p => p.Category, opt => opt.Ignore()); 16 | // } 17 | // } 18 | 19 | // public class UserCustomMapping : IHaveCustomMapping 20 | // { 21 | // public void CreateMappings(Profile profile) 22 | // { 23 | // profile.CreateMap().ReverseMap(); 24 | // } 25 | // } 26 | 27 | // public class CategoryCustomMapping : IHaveCustomMapping 28 | // { 29 | // public void CreateMappings(Profile profile) 30 | // { 31 | // profile.CreateMap().ReverseMap(); 32 | // } 33 | // } 34 | //} 35 | -------------------------------------------------------------------------------- /MyApi/Models/PostDto.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Entities; 3 | using System; 4 | using WebFramework.Api; 5 | 6 | namespace MyApi.Models 7 | { 8 | public class PostDto : BaseDto 9 | { 10 | public string Title { get; set; } 11 | public string Description { get; set; } 12 | public int CategoryId { get; set; } 13 | public int AuthorId { get; set; } 14 | } 15 | 16 | public class PostSelectDto : BaseDto 17 | { 18 | public string Title { get; set; } 19 | public string Description { get; set; } 20 | public string CategoryName { get; set; } //=> Category.Name 21 | public string AuthorFullName { get; set; } //=> Author.FullName 22 | public string FullTitle { get; set; } // => mapped from "Title (Category.Name)" 23 | 24 | public override void CustomMappings(IMappingExpression mappingExpression) 25 | { 26 | mappingExpression.ForMember( 27 | dest => dest.FullTitle, 28 | config => config.MapFrom(src => $"{src.Title} ({src.Category.Name})")); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MyApi/Models/TokenRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace MyApi.Models 4 | { 5 | public class TokenRequest 6 | { 7 | [Required] 8 | public string grant_type { get; set; } 9 | public string username { get; set; } 10 | public string password { get; set; } 11 | public string refresh_token { get; set; } 12 | public string scope { get; set; } 13 | 14 | public string client_id { get; set; } 15 | public string client_secret { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /MyApi/Models/UserDto.cs: -------------------------------------------------------------------------------- 1 | using Entities; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using WebFramework.Api; 6 | 7 | namespace MyApi.Models 8 | { 9 | public class UserDto : BaseDto, IValidatableObject 10 | { 11 | [Required] 12 | [StringLength(100)] 13 | public string UserName { get; set; } 14 | 15 | [Required] 16 | [StringLength(100)] 17 | public string Email { get; set; } 18 | 19 | [Required] 20 | [StringLength(500)] 21 | public string Password { get; set; } 22 | 23 | [Required] 24 | [StringLength(100)] 25 | public string FullName { get; set; } 26 | 27 | public int Age { get; set; } 28 | 29 | public GenderType Gender { get; set; } 30 | 31 | public IEnumerable Validate(ValidationContext validationContext) 32 | { 33 | if (UserName.Equals("test", StringComparison.OrdinalIgnoreCase)) 34 | yield return new ValidationResult("نام کاربری نمیتواند Test باشد", new[] { nameof(UserName) }); 35 | if (Password.Equals("123456")) 36 | yield return new ValidationResult("رمز عبور نمیتواند 123456 باشد", new[] { nameof(Password) }); 37 | if (Gender == GenderType.Male && Age > 30) 38 | yield return new ValidationResult("آقایان بیشتر از 30 سال معتبر نیستند", new[] { nameof(Gender), nameof(Age) }); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MyApi/MyApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 9.0 6 | MyApi.xml 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /MyApi/MyApi.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ProjectDebugger 5 | 6 | 7 | IIS Express 8 | false 9 | false 10 | c:\users\ebrahimi\appdata\local\microsoft\visualstudio\16.0_0a2fca5c\extensions\bazl1bqd.pxh\tmcc\ 11 | c:\users\ebrahimi\appdata\local\microsoft\visualstudio\16.0_0a2fca5c\extensions\bazl1bqd.pxh\tmcc\RevDeBug.ImportAfter.targets 12 | false 13 | true 14 | true 15 | C:\Users\Ebrahimi\Documents\RevDeBug\MyApi\Metadata\\ 16 | Continuous 17 | false 18 | 127.0.0.1 19 | 42742 20 | 21 | -------------------------------------------------------------------------------- /MyApi/MyApi.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MyApi 5 | 6 | 7 | 8 | 9 | 3-letter ISO country code 10 | 11 | Iran 12 | 13 | 14 | 15 | Name of city 16 | 17 | Seattle 18 | 19 | 20 | 21 | This method generate JWT Token 22 | 23 | The information of token request 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /MyApi/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Autofac.Extensions.DependencyInjection; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.Extensions.Logging; 8 | using NLog; 9 | using NLog.Config; 10 | using NLog.Targets; 11 | using NLog.Web; 12 | using Sentry; 13 | 14 | namespace MyApi 15 | { 16 | public class Program 17 | { 18 | public static async Task Main(string[] args) 19 | { 20 | #region Sentry/NLog 21 | 22 | //We used NLog.Targets.Sentry2 library formerly 23 | //But it was not based on NetStandard and used an unstable SharpRaven library 24 | //So we decided to replace it with a better library 25 | //The NLog.Targets.Sentry3 library supports NetStandard2.0 and uses an updated version of SharpRaven library. 26 | //But Sentry.NLog is the official sentry library integrated with nlog and better than all others. 27 | 28 | //NLog.Targets.Sentry3 29 | //https://github.com/CurtisInstruments/NLog.Targets.Sentry 30 | 31 | //Sentry SDK for .NET 32 | //https://github.com/getsentry/sentry-dotnet 33 | 34 | //Sample integration of NLog with Sentry 35 | //https://github.com/getsentry/sentry-dotnet/tree/master/samples/Sentry.Samples.NLog 36 | 37 | 38 | //Set deafult proxy 39 | WebRequest.DefaultWebProxy = new WebProxy("http://127.0.0.1:8118", true) { UseDefaultCredentials = true }; 40 | 41 | // You can configure your logger using a configuration file: 42 | 43 | // If using an NLog.config xml file, NLog will load the configuration automatically Or, if using a 44 | // different file, you can call the following to load it for you: 45 | //LogManager.Configuration = LogManager.LoadConfiguration("NLog-file.config").Configuration; 46 | 47 | var logger = LogManager.GetCurrentClassLogger(); 48 | 49 | // Or you can configure it with code: 50 | //UsingCodeConfiguration(); 51 | 52 | #endregion 53 | 54 | try 55 | { 56 | logger.Debug("init main"); 57 | await CreateHostBuilder(args).Build().RunAsync(); 58 | } 59 | catch (Exception ex) 60 | { 61 | //NLog: catch setup errors 62 | logger.Error(ex, "Stopped program because of exception"); 63 | throw; 64 | } 65 | finally 66 | { 67 | // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux) 68 | LogManager.Flush(); 69 | LogManager.Shutdown(); 70 | } 71 | } 72 | 73 | public static IHostBuilder CreateHostBuilder(string[] args) => 74 | Host.CreateDefaultBuilder(args) 75 | .UseServiceProviderFactory(new AutofacServiceProviderFactory()) 76 | .ConfigureLogging(options => options.ClearProviders()) 77 | .UseNLog() 78 | .ConfigureWebHostDefaults(webBuilder => 79 | { 80 | //webBuilder.ConfigureLogging(options => options.ClearProviders()); 81 | //webBuilder.UseNLog(); 82 | webBuilder.UseStartup(); 83 | }); 84 | 85 | private static void UsingCodeConfiguration() 86 | { 87 | // Other overloads exist, for example, configure the SDK with only the DSN or no parameters at all. 88 | var config = new LoggingConfiguration(); 89 | 90 | config.AddSentry(options => 91 | { 92 | options.Layout = "${message}"; 93 | options.BreadcrumbLayout = "${logger}: ${message}"; // Optionally specify a separate format for breadcrumbs 94 | 95 | options.MinimumBreadcrumbLevel = NLog.LogLevel.Debug; // Debug and higher are stored as breadcrumbs (default is Info) 96 | options.MinimumEventLevel = NLog.LogLevel.Error; // Error and higher is sent as event (default is Error) 97 | 98 | // If DSN is not set, the SDK will look for an environment variable called SENTRY_DSN. If 99 | // nothing is found, SDK is disabled. 100 | options.Dsn = "https://a48f67497c814561aca2c66fa5ee37fc:a5af1a051d6f4f09bdd82472d5c2629d@sentry.io/1340240"; 101 | 102 | options.AttachStacktrace = true; 103 | options.SendDefaultPii = true; // Send Personal Identifiable information like the username of the user logged in to the device 104 | 105 | options.IncludeEventDataOnBreadcrumbs = true; // Optionally include event properties with breadcrumbs 106 | options.ShutdownTimeoutSeconds = 5; 107 | 108 | options.AddTag("logger", "${logger}"); // Send the logger name as a tag 109 | 110 | options.HttpProxy = new WebProxy("http://127.0.0.1:8118", true) { UseDefaultCredentials = true }; 111 | // Other configuration 112 | }); 113 | 114 | config.AddTarget(new DebuggerTarget("Debugger")); 115 | config.AddTarget(new ColoredConsoleTarget("Console")); 116 | 117 | config.AddRuleForAllLevels("Console"); 118 | config.AddRuleForAllLevels("Debugger"); 119 | 120 | LogManager.Configuration = config; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /MyApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56966", 7 | "sslPort": 44339 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "MyApi": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /MyApi/Startup.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Common; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using WebFramework.Configuration; 8 | using WebFramework.CustomMapping; 9 | using WebFramework.Middlewares; 10 | using WebFramework.Swagger; 11 | 12 | namespace MyApi 13 | { 14 | public class Startup 15 | { 16 | private readonly SiteSettings _siteSetting; 17 | public IConfiguration Configuration { get; } 18 | 19 | public Startup(IConfiguration configuration) 20 | { 21 | Configuration = configuration; 22 | 23 | _siteSetting = configuration.GetSection(nameof(SiteSettings)).Get(); 24 | } 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.Configure(Configuration.GetSection(nameof(SiteSettings))); 30 | 31 | services.InitializeAutoMapper(); 32 | 33 | services.AddDbContext(Configuration); 34 | 35 | services.AddCustomIdentity(_siteSetting.IdentitySettings); 36 | 37 | services.AddMinimalMvc(); 38 | 39 | services.AddElmahCore(Configuration, _siteSetting); 40 | 41 | services.AddJwtAuthentication(_siteSetting.JwtSettings); 42 | 43 | services.AddCustomApiVersioning(); 44 | 45 | services.AddSwagger(); 46 | 47 | // Don't create a ContainerBuilder for Autofac here, and don't call builder.Populate() 48 | // That happens in the AutofacServiceProviderFactory for you. 49 | } 50 | 51 | // ConfigureContainer is where you can register things directly with Autofac. 52 | // This runs after ConfigureServices so the things ere will override registrations made in ConfigureServices. 53 | // Don't build the container; that gets done for you by the factory. 54 | public void ConfigureContainer(ContainerBuilder builder) 55 | { 56 | //Register Services to Autofac ContainerBuilder 57 | builder.AddServices(); 58 | } 59 | 60 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 61 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 62 | { 63 | app.IntializeDatabase(); 64 | 65 | app.UseCustomExceptionHandler(); 66 | 67 | app.UseHsts(env); 68 | 69 | app.UseHttpsRedirection(); 70 | 71 | app.UseElmahCore(_siteSetting); 72 | 73 | app.UseSwaggerAndUI(); 74 | 75 | app.UseRouting(); 76 | 77 | app.UseAuthentication(); 78 | app.UseAuthorization(); 79 | 80 | //Use this config just in Develoment (not in Production) 81 | //app.UseCors(config => config.AllowAnyHeader().AllowAnyMethod().AllowAnyOrigin()); 82 | 83 | app.UseEndpoints(config => 84 | { 85 | config.MapControllers(); // Map attribute routing 86 | // .RequireAuthorization(); Apply AuthorizeFilter as global filter to all endpoints 87 | //config.MapDefaultControllerRoute(); // Map default route {controller=Home}/{action=Index}/{id?} 88 | }); 89 | 90 | //Using 'UseMvc' to configure MVC is not supported while using Endpoint Routing. 91 | //To continue using 'UseMvc', please set 'MvcOptions.EnableEndpointRouting = false' inside 'ConfigureServices'. 92 | //app.UseMvc(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /MyApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "SqlServer": "Data Source=.;Initial Catalog=MyApiDb;Integrated Security=true", 4 | "Elmah": "Data Source=.;Initial Catalog=MyApiDb;Integrated Security=true" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Debug", 9 | "System": "Information", 10 | "Microsoft": "Information" 11 | } 12 | }, 13 | "SiteSettings": { 14 | "ElmahPath": "/elmah-errors", 15 | "JwtSettings": { 16 | "SecretKey": "LongerThan-16Char-SecretKey", 17 | "EncryptKey": "16CharEncryptKey", 18 | "Issuer": "MyWebsite", 19 | "Audience": "MyWebsite", 20 | "NotBeforeMinutes": "0", 21 | "ExpirationMinutes": "60" 22 | }, 23 | "IdentitySettings": { 24 | "PasswordRequireDigit": "true", 25 | "PasswordRequiredLength": "6", 26 | "PasswordRequireNonAlphanumeric": "false", 27 | "PasswordRequireUppercase": "false", 28 | "PasswordRequireLowercase": "false", 29 | "RequireUniqueEmail": "true" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /MyApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "SqlServer": "Data Source=.;Initial Catalog=MyApiDb;Integrated Security=true", 4 | "Elmah": "Data Source=.;Initial Catalog=MyApiDb;Integrated Security=true" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Warning" 9 | } 10 | }, 11 | "SiteSettings": { 12 | "ElmahPath": "/elmah-errors", 13 | "JwtSettings": { 14 | "SecretKey": "LongerThan-16Char-SecretKey", 15 | "EncryptKey": "16CharEncryptKey", 16 | "Issuer": "MyWebsite", 17 | "Audience": "MyWebsite", 18 | "NotBeforeMinutes": "0", 19 | "ExpirationMinutes": "60" 20 | }, 21 | "IdentitySettings": { 22 | "PasswordRequireDigit": "true", 23 | "PasswordRequiredLength": "6", 24 | "PasswordRequireNonAlphanumeric": "false", 25 | "PasswordRequireUppercase": "false", 26 | "PasswordRequireLowercase": "false", 27 | "RequireUniqueEmail": "true" 28 | } 29 | }, 30 | "AllowedHosts": "*" 31 | } 32 | -------------------------------------------------------------------------------- /MyApi/nlog.config: -------------------------------------------------------------------------------- 1 |  2 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 35 | 36 | true 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🥇Professional REST API design with ASP.NET Core WebAPI 4 | 5 | This project is an example of lightweight and extensible infrastructure for building RESTful Web API with ASP.NET Core. 6 | 7 | This example contains a number of tricks and techniques that is the result of many years of my experience in WebAPI/RESTful programming in ASP.NET Core 8 | 9 | If you want a total deep dive on REST, API security, ASP.NET Core and much more, check out my [Course](http://beyamooz.com/project-based-aspnet/%D8%AF%D9%88%D8%B1%D9%87-api-%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%D8%A7%D8%B5%D9%88%D9%84%DB%8C-%D9%88-%D8%AD%D8%B1%D9%81%D9%87-%D8%A7%DB%8C-%D8%AF%D8%B1-asp-net-core). 10 | 11 | ## Testing it out 12 | 1. Clone or download this repository 13 | 2. Build the solution using command line with `dotnet build` 14 | 3. Go to **MyApi** directory and run project using command line with `dotnet run` 15 | 4. Browse to this url https://localhost:5001/swagger to see SwaggerUI page 16 | 17 | ## Techniques and Features 18 | - JWT Authentication 19 | - Secure JWT using Encryption (JWE) 20 | - Logging to File, Console and Database using [Elmah](https://github.com/ElmahCore/ElmahCore) & [NLog](https://github.com/NLog/NLog.Web) 21 | - Logging to [sentry.io](sentry.io) (Log Management System) 22 | - Exception Handling using Custom Middleware 23 | - Automatic Validation 24 | - Standard API Resulting 25 | - Dependency Injection using [Autofac (Ioc Container)](https://github.com/autofac/Autofac) 26 | - Map resources using [AutoMapper](https://github.com/AutoMapper/AutoMapper) 27 | - Async/Await Best Practices 28 | - Versioning Management 29 | - Using [Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) (Swashbuckle) 30 | - Auto Document Generator for Swagger 31 | - Integrate Swagger and Versioning 32 | - Integrate Swagger and JWT/OAuth Authentication 33 | - Best Practices for Performance and Security 34 | 35 | ## Give a Star! ⭐️ 36 | If you like this project, learn something or you are using it in your applications, please give it a star. Thanks! 37 | 38 |
39 | 40 | --- 41 | 42 |
43 | 44 | # 🥇پروژه دوره API نویسی اصولی و حرفه ای در ASP.NET Core 45 | 46 | [در این دوره همه نکات مهم و پرکاربرد در API نویسی اصولی و حرفه ای در ASP.NET Core بررسی شده اند.](http://beyamooz.com/project-based-aspnet/%D8%AF%D9%88%D8%B1%D9%87-api-%D9%86%D9%88%DB%8C%D8%B3%DB%8C-%D8%A7%D8%B5%D9%88%D9%84%DB%8C-%D9%88-%D8%AD%D8%B1%D9%81%D9%87-%D8%A7%DB%8C-%D8%AF%D8%B1-asp-net-core) 47 | 48 | در این دوره سعی شده بهترین و محبوب ترین تکنولوژی ها، کتابخانه ها و ابزار ها داخل پروژه استفاده بشه. همچنین Best Practice های پرفرمنسی و امنیتی بعلاوه تکنیک های پرکاربرد را بررسی و در قالب یک معماری حرفه ای و اصولی استفاده می کنیم. 49 | 50 | **توجه:** 51 | - **برنچ master این مخزن همواره به آخرین نسخه ASP.NET Core (به همراه تمام Dependency هایش) بروز رسانی شده و خواهد شد (در حال حاضر ASP.NET Core 6.0 می باشد)** 52 | - **جهت دسترسی به کد اولیه پروژه که با ASP.NET Core 2.1 در هنگام تهیه دوره نوشته بود [به این برنچ مراجعه کنید](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/tree/AspNetCore2.1)** 53 | - **جهت دسترسی به کد پروژه در ورژن ASP.NET Core 3.1 [به این برنچ مراجعه کنید](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/tree/AspNetCore3.1)** 54 | - **جهت دسترسی به کد پروژه در ورژن ASP.NET Core 5.0 [به این برنچ مراجعه کنید](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/tree/AspNetCore5.0)** 55 | - **همچنین جهت اطلاعات بیشتر از تغییرات که به هنگام Upgrade پروژه از نسخه 2.1 به 3.1 انجام شد، میتونین به قسمت [ChangeLog](https://github.com/dotnetzoom/AspNetCore-WebApi-Course/blob/master/CHANGELOG.md) مراجعه کنید** 56 | 57 | ## تکنولوژی، ابزار ها و قابلیت ها 58 | - **لایه بندی اصولی پروژه (Project Layering and Architecture)** : در این دوره لایه بندی اصولی یک پروژه را از ابتدا شروع و هر بخش را بررسی می کنیم. همچنین مباحث Repository و UOW رو هم بررسی می کنیم. 59 | - **احراز هویت (Authentication)** 60 | - **ASP.NET Core Identity** : احراز هویت توسط Identity + سفارشی سازی 61 | - **(Json Web Token) JWT** : احراز هویت توسط Jwt + یکپارچه سازی آن با Identity 62 | - **(Json Web Encryption) JWE** : ایمن سازی توکن ها بوسیله [رمزنگاری توکن (JWE)](https://www.dotnettips.info/post/2992) 63 | - **Security Stamp** : جلوگیری از اعتبارسنجی توکن به هنگام تغییر دسترسی های کاربر جهت امنیت بیشتر 64 | - **Claims** : کار با Claim ها و تولید خودکار آنها توسط ClaimsFactory 65 | - **Logging (ثبت خطا ها)** 66 | - **Elmah** : استفاده از [Elmah](https://github.com/ElmahCore/ElmahCore) برای لاگ خطا ها در Memory, XML File و Database 67 | - **NLog** : استفاده از [NLog](https://github.com/NLog/NLog.Web) برای لاگ خطا ها در File و Console 68 | - **Custom Middleware** : نوشتن یک میدلویر سفارشی جهت لاگ تمامی خطا (Exception) ها 69 | - **Custom Exception** : نوشتن Exception برای مدیریت ساده تر خطا ها 70 | - **Sentry** : ثبت خطا ها در سیستم مدیریت لاگ [sentry.io](sentry.io) (مناسب برای پروژه های بزرگ) 71 | - **تزریق وابستگی (Dependency Injection**) 72 | - **ASP.NET Core IOC Container** : استفاده از Ioc container داخلی Asp Core 73 | - **Autofac** : استفاده از محبوب ترین کتابخانه [Autofac (Ioc Container)](https://github.com/autofac/Autofac) 74 | - **Auto Registeration** : ثبت خودکار سرویس ها توسط یک تکنیک خلاقانه با کمک Autofac 75 | - **ارتباط با دیتابیس (Data Access)** 76 | - **Entity Framework Core** : استفاده از EF Core 77 | - **Auto Entity Registration** : ثبت Entity های DbContext به صورت خودکار توسط Reflection 78 | - **Pluralizing Table Name** : جمع بندی نام جداول EF Core به صورت خودکار توسط کتابخانه [Pluralize.NET](https://github.com/sarathkcm/Pluralize.NET) و Reflection 79 | - **Automatic Configuration** : اعمال کانفیگ های EntityTypeConfiguration (FluentApi) به صورت خودکار توسط Reflection 80 | - **Sequential Guid** : بهینه سازی مقدار دهی identity برای Guid به صورت خودکار توسط Reflection 81 | - **Repository** : توضیحاتی در مورد معماری اصولی Repository در EF Core 82 | - **Data Intitializer** : یک معماری اصولی برای Seed کردن مقادیر اولیه به Database 83 | - **Auto Migrate** : آپدیت Database به آخرین Migration به صورت خودکار 84 | - **Clean String** : اصلاح و یک دست سازی حروف "ی" و "ک" عربی به فارسی و encoding اعداد فارسی در DbContext به صورت خودکار به هنگام SaveChanges 85 | - **Versioning** : نسخه بندی و مدیریت نسخه های پروژه + سفارشی سازی و ایجاد یک معماری حرفه ای 86 | - **ابزار (Swashbuckle) Swagger** 87 | - **Swagger UI** : ساخت یک ظاهر شکیل به همراه داکیومنت Aciton ها و Controller های پروژه و امکان تست API ها توسط [Swagger](https://github.com/domaindrivendev/Swashbuckle.AspNetCore) UI 88 | - **Versioning** : یکپارچه سازی اصولی Swagger با سیستم نسخه گذاری (Versioning) 89 | - **JWT Authentication** : یکپارچه سازی Swagger با سیستم احراز هویت بر اساس Jwt 90 | - **OAuth Authentication** : یکپارچه سازی Swagger با سیستم احراز هویت بر اساس OAuth 91 | - **Auto Summary Document Generation** : تولید خودکار داکیومنت (توضیحات) برای API های پروژه 92 | - **Advanced Customization** : سفارشی سازی های پیشرفته در Swagger 93 | - **دیگر قابلیت ها** 94 | - **API Standard Resulting** : استاندارد سازی و یک دست سازی خروجی API ها توسط ActionFilter 95 | - **Automatic Model Validation** : اعتبار سنجی خودکار 96 | - **AutoMapper** : جهت Mapping اشیاء توسط کتابخانه محبوب [AutoMapper](https://github.com/AutoMapper/AutoMapper) 97 | - **Auto Mapping** : سفارشی سازی وایجاد [یک معماری حرفه ای](https://github.com/mjebrahimi/auto-mapping) برای Mapping اشیا توسط Reflection 98 | - **Generic Controller** : ساخت کنترلر برای عملیات CRUD بدون کد نویسی توسط ارث بری از CrudController 99 | - **Site Setting** : مدیریت تنظیمات پروژ توسط Configuration و ISnapshotOptions 100 | - **Postman** : آشنایی و کار با Postman جهت تست API ها 101 | - **Minimal Mvc** : حذف سرویس های اضافه MVC برای افزایش پرفرمنس در API نویسی 102 | - **Best Practices** : اعمال Best Practices ها جهت بهینه سازی، افزایش پرفرمنس و کدنویسی تمیز و اصولی 103 | - **و چندین نکته مفید دیگر ...** 104 | 105 | ## مزیت اصلی این دوره؟ 106 | به جای اینکه ماه ها وقت صرف کنین تحقیق کنین، مطالعه کنین و موارد کاربردی و مهم API نویسی رو یاد بگیرین 107 | توی این دوره همشو یک جا و سریع یاد میگیرین و تو وقتتون صرفه جویی میشه. همچنین یک پله هم به Senior Developer شدن نزدیک میشین ;) 108 | 109 | ## دانلود ویدئو های آموزشی دوره 110 | این دوره در قالب (در مجموع) 22 ساعت آموزش ویدئویی توسط محمد جواد ابراهیمی ([mjebrahimi](https://github.com/mjebrahimi)) تدریس شده است. 111 | 112 | **لینک دانلود** : [خرید از سایت بیاموز (کد تخفیف 20 درصدی : **DotNetZoom**)](http://beyamooz.com/project-based-aspnet/دوره-api-نویسی-اصولی-و-حرفه-ای-در-asp-net-core) 113 | 114 | ## پیش نیاز این دوره : 115 | سطح دوره پیشرفته بوده و برای افراد **مبتدی** مناسب **نیست**. 116 | 117 | این دوره، آموزش ASP.NET Core نیست و زیاد روی مباحثش عمیق نمیشیم و فقط به مباحثی می پردازیم که مرتبط با API نویسی توی ASP.NET Core هستش. 118 | 119 | انتظار میره برای شروع این دوره پیش نیاز های زیر رو داشته باشین : 120 | 121 | 1. تسلط نسبی بر روی زبان سی شارپ 122 | 2. آشنایی با پروتکل Http و REST 123 | 3. آشنایی با Entity Framework (ترجیحا EF Core) 124 | 4. آشنایی با ASP.NET MVC یا ASP.NET Core (و ترجیحا آشنایی با WebAPI) 125 | 126 | ## ارتباط با مدرس 127 | جهت ارتباط با مدرس و ارائه هرگونه پیشنهاد، انتقاد، نظر و سوالاتتون میتونین با اکانت تلگرام **محمد جواد ابراهیمی** در ارتباط باشین [**mjebrahimi@**](https://t.me/mjebrahimi) 128 | 129 | ## حمایت از ما 130 | ⭐️در پایان اگه واقعا از دوره **خوشتون** اومده بود حتما بهش **Star** بدین 131 | . با اینکار حمایت خودتون رو از ما اعلام میکنین🙏 و این به ما انگیزه میده آموزش های بیشتری تهیه کنیم✌ 132 | 133 |
134 | -------------------------------------------------------------------------------- /Services/AccessToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | 4 | namespace Services 5 | { 6 | public class AccessToken 7 | { 8 | public string access_token { get; set; } 9 | public string refresh_token { get; set; } 10 | public string token_type { get; set; } 11 | public int expires_in { get; set; } 12 | 13 | public AccessToken(JwtSecurityToken securityToken) 14 | { 15 | access_token = new JwtSecurityTokenHandler().WriteToken(securityToken); 16 | token_type = "Bearer"; 17 | expires_in = (int)(securityToken.ValidTo - DateTime.UtcNow).TotalSeconds; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Services/DataInitializer/CategoryDataInitializer.cs: -------------------------------------------------------------------------------- 1 | using Data.Repositories; 2 | using Entities; 3 | using System.Linq; 4 | 5 | namespace Services.DataInitializer 6 | { 7 | public class CategoryDataInitializer : IDataInitializer 8 | { 9 | private readonly IRepository repository; 10 | 11 | public CategoryDataInitializer(IRepository repository) 12 | { 13 | this.repository = repository; 14 | } 15 | 16 | public void InitializeData() 17 | { 18 | if (!repository.TableNoTracking.Any(p => p.Name == "دسته بندی اولیه 1")) 19 | { 20 | repository.Add(new Category 21 | { 22 | Name = "دسته بندی اولیه 1" 23 | }); 24 | } 25 | if (!repository.TableNoTracking.Any(p => p.Name == "دسته بندی اولیه 2")) 26 | { 27 | repository.Add(new Category 28 | { 29 | Name = "دسته بندی اولیه 2" 30 | }); 31 | } 32 | if (!repository.TableNoTracking.Any(p => p.Name == "دسته بندی اولیه 3")) 33 | { 34 | repository.Add(new Category 35 | { 36 | Name = "دسته بندی اولیه 3" 37 | }); 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Services/DataInitializer/IDataInitializer.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | 3 | namespace Services.DataInitializer 4 | { 5 | public interface IDataInitializer : IScopedDependency 6 | { 7 | void InitializeData(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Services/DataInitializer/UserDataInitializer.cs: -------------------------------------------------------------------------------- 1 | using Entities; 2 | using Microsoft.AspNetCore.Identity; 3 | using Microsoft.EntityFrameworkCore; 4 | using System.Linq; 5 | 6 | namespace Services.DataInitializer 7 | { 8 | public class UserDataInitializer : IDataInitializer 9 | { 10 | private readonly UserManager userManager; 11 | private readonly RoleManager roleManager; 12 | 13 | public UserDataInitializer(UserManager userManager, RoleManager roleManager) 14 | { 15 | this.userManager = userManager; 16 | this.roleManager = roleManager; 17 | } 18 | 19 | public void InitializeData() 20 | { 21 | if (!roleManager.RoleExistsAsync("Admin").GetAwaiter().GetResult()) 22 | { 23 | roleManager.CreateAsync(new Role { Name = "Admin", Description = "Admin role" }).GetAwaiter().GetResult(); 24 | } 25 | if (!userManager.Users.AsNoTracking().Any(p => p.UserName == "Admin")) 26 | { 27 | var user = new User 28 | { 29 | Age = 25, 30 | FullName = "محمد جوادابراهیمی", 31 | Gender = GenderType.Male, 32 | UserName = "admin", 33 | Email = "admin@site.com" 34 | }; 35 | userManager.CreateAsync(user, "123456").GetAwaiter().GetResult(); 36 | userManager.AddToRoleAsync(user, "Admin").GetAwaiter().GetResult(); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Services/Services.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Services/Services/IJwtService.cs: -------------------------------------------------------------------------------- 1 | using Entities; 2 | using System.Threading.Tasks; 3 | 4 | namespace Services 5 | { 6 | public interface IJwtService 7 | { 8 | Task GenerateAsync(User user); 9 | } 10 | } -------------------------------------------------------------------------------- /Services/Services/JwtService.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Entities; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.Extensions.Options; 5 | using Microsoft.IdentityModel.Tokens; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IdentityModel.Tokens.Jwt; 9 | using System.Security.Claims; 10 | using System.Text; 11 | using System.Threading.Tasks; 12 | 13 | namespace Services 14 | { 15 | public class JwtService : IJwtService, IScopedDependency 16 | { 17 | private readonly SiteSettings _siteSetting; 18 | private readonly SignInManager signInManager; 19 | 20 | public JwtService(IOptionsSnapshot settings, SignInManager signInManager) 21 | { 22 | _siteSetting = settings.Value; 23 | this.signInManager = signInManager; 24 | } 25 | 26 | public async Task GenerateAsync(User user) 27 | { 28 | var secretKey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.SecretKey); // longer that 16 character 29 | var signingCredentials = new SigningCredentials(new SymmetricSecurityKey(secretKey), SecurityAlgorithms.HmacSha256Signature); 30 | 31 | var encryptionkey = Encoding.UTF8.GetBytes(_siteSetting.JwtSettings.EncryptKey); //must be 16 character 32 | var encryptingCredentials = new EncryptingCredentials(new SymmetricSecurityKey(encryptionkey), SecurityAlgorithms.Aes128KW, SecurityAlgorithms.Aes128CbcHmacSha256); 33 | 34 | var claims = await _getClaimsAsync(user); 35 | 36 | var descriptor = new SecurityTokenDescriptor 37 | { 38 | Issuer = _siteSetting.JwtSettings.Issuer, 39 | Audience = _siteSetting.JwtSettings.Audience, 40 | IssuedAt = DateTime.Now, 41 | NotBefore = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.NotBeforeMinutes), 42 | Expires = DateTime.Now.AddMinutes(_siteSetting.JwtSettings.ExpirationMinutes), 43 | SigningCredentials = signingCredentials, 44 | EncryptingCredentials = encryptingCredentials, 45 | Subject = new ClaimsIdentity(claims) 46 | }; 47 | 48 | //JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); 49 | //JwtSecurityTokenHandler.DefaultMapInboundClaims = false; 50 | //JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear(); 51 | 52 | var tokenHandler = new JwtSecurityTokenHandler(); 53 | 54 | var securityToken = tokenHandler.CreateJwtSecurityToken(descriptor); 55 | 56 | //string encryptedJwt = tokenHandler.WriteToken(securityToken); 57 | 58 | return new AccessToken(securityToken); 59 | } 60 | 61 | private async Task> _getClaimsAsync(User user) 62 | { 63 | var result = await signInManager.ClaimsFactory.CreateAsync(user); 64 | //add custom claims 65 | var list = new List(result.Claims); 66 | list.Add(new Claim(ClaimTypes.MobilePhone, "09123456987")); 67 | 68 | //JwtRegisteredClaimNames.Sub 69 | //var securityStampClaimType = new ClaimsIdentityOptions().SecurityStampClaimType; 70 | 71 | //var list = new List 72 | //{ 73 | // new Claim(ClaimTypes.Name, user.UserName), 74 | // new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), 75 | // //new Claim(ClaimTypes.MobilePhone, "09123456987"), 76 | // //new Claim(securityStampClaimType, user.SecurityStamp.ToString()) 77 | //}; 78 | 79 | //var roles = new Role[] { new Role { Name = "Admin" } }; 80 | //foreach (var role in roles) 81 | // list.Add(new Claim(ClaimTypes.Role, role.Name)); 82 | 83 | return list; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /WebFramework/Api/ApiResult.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Common.Utilities; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Newtonsoft.Json; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace WebFramework.Api 9 | { 10 | public class ApiResult 11 | { 12 | public bool IsSuccess { get; set; } 13 | public ApiResultStatusCode StatusCode { get; set; } 14 | 15 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 16 | public string Message { get; set; } 17 | 18 | public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, string message = null) 19 | { 20 | IsSuccess = isSuccess; 21 | StatusCode = statusCode; 22 | Message = message ?? statusCode.ToDisplay(); 23 | } 24 | 25 | #region Implicit Operators 26 | public static implicit operator ApiResult(OkResult result) 27 | { 28 | return new ApiResult(true, ApiResultStatusCode.Success); 29 | } 30 | 31 | public static implicit operator ApiResult(BadRequestResult result) 32 | { 33 | return new ApiResult(false, ApiResultStatusCode.BadRequest); 34 | } 35 | 36 | public static implicit operator ApiResult(BadRequestObjectResult result) 37 | { 38 | var message = result.Value?.ToString(); 39 | if (result.Value is SerializableError errors) 40 | { 41 | var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct(); 42 | message = string.Join(" | ", errorMessages); 43 | } 44 | return new ApiResult(false, ApiResultStatusCode.BadRequest, message); 45 | } 46 | 47 | public static implicit operator ApiResult(ContentResult result) 48 | { 49 | return new ApiResult(true, ApiResultStatusCode.Success, result.Content); 50 | } 51 | 52 | public static implicit operator ApiResult(NotFoundResult result) 53 | { 54 | return new ApiResult(false, ApiResultStatusCode.NotFound); 55 | } 56 | #endregion 57 | } 58 | 59 | public class ApiResult : ApiResult 60 | where TData : class 61 | { 62 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] 63 | public TData Data { get; set; } 64 | 65 | public ApiResult(bool isSuccess, ApiResultStatusCode statusCode, TData data, string message = null) 66 | : base(isSuccess, statusCode, message) 67 | { 68 | Data = data; 69 | } 70 | 71 | #region Implicit Operators 72 | public static implicit operator ApiResult(TData data) 73 | { 74 | return new ApiResult(true, ApiResultStatusCode.Success, data); 75 | } 76 | 77 | public static implicit operator ApiResult(OkResult result) 78 | { 79 | return new ApiResult(true, ApiResultStatusCode.Success, null); 80 | } 81 | 82 | public static implicit operator ApiResult(OkObjectResult result) 83 | { 84 | return new ApiResult(true, ApiResultStatusCode.Success, (TData)result.Value); 85 | } 86 | 87 | public static implicit operator ApiResult(BadRequestResult result) 88 | { 89 | return new ApiResult(false, ApiResultStatusCode.BadRequest, null); 90 | } 91 | 92 | public static implicit operator ApiResult(BadRequestObjectResult result) 93 | { 94 | var message = result.Value?.ToString(); 95 | if (result.Value is SerializableError errors) 96 | { 97 | var errorMessages = errors.SelectMany(p => (string[])p.Value).Distinct(); 98 | message = string.Join(" | ", errorMessages); 99 | } 100 | return new ApiResult(false, ApiResultStatusCode.BadRequest, null, message); 101 | } 102 | 103 | public static implicit operator ApiResult(ContentResult result) 104 | { 105 | return new ApiResult(true, ApiResultStatusCode.Success, null, result.Content); 106 | } 107 | 108 | public static implicit operator ApiResult(NotFoundResult result) 109 | { 110 | return new ApiResult(false, ApiResultStatusCode.NotFound, null); 111 | } 112 | 113 | public static implicit operator ApiResult(NotFoundObjectResult result) 114 | { 115 | return new ApiResult(false, ApiResultStatusCode.NotFound, (TData)result.Value); 116 | } 117 | #endregion 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /WebFramework/Api/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using WebFramework.Filters; 3 | 4 | namespace WebFramework.Api 5 | { 6 | [ApiController] 7 | //[AllowAnonymous] 8 | [ApiResultFilter] 9 | [Route("api/v{version:apiVersion}/[controller]")]// api/v1/[controller] 10 | public class BaseController : ControllerBase 11 | { 12 | //public UserRepository UserRepository { get; set; } => property injection 13 | public bool UserIsAutheticated => HttpContext.User.Identity.IsAuthenticated; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /WebFramework/Api/BaseDto.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Entities; 3 | using System.ComponentModel.DataAnnotations; 4 | using WebFramework.CustomMapping; 5 | 6 | namespace WebFramework.Api 7 | { 8 | public abstract class BaseDto : IHaveCustomMapping 9 | where TDto : class, new() 10 | where TEntity : class, IEntity, new() 11 | { 12 | [Display(Name = "ردیف")] 13 | public TKey Id { get; set; } 14 | 15 | public TEntity ToEntity(IMapper mapper) 16 | { 17 | return mapper.Map(CastToDerivedClass(mapper, this)); 18 | } 19 | 20 | public TEntity ToEntity(IMapper mapper, TEntity entity) 21 | { 22 | return mapper.Map(CastToDerivedClass(mapper, this), entity); 23 | } 24 | 25 | public static TDto FromEntity(IMapper mapper, TEntity model) 26 | { 27 | return mapper.Map(model); 28 | } 29 | 30 | protected TDto CastToDerivedClass(IMapper mapper, BaseDto baseInstance) 31 | { 32 | return mapper.Map(baseInstance); 33 | } 34 | 35 | public void CreateMappings(Profile profile) 36 | { 37 | var mappingExpression = profile.CreateMap(); 38 | 39 | var dtoType = typeof(TDto); 40 | var entityType = typeof(TEntity); 41 | //Ignore any property of source (like Post.Author) that dose not contains in destination 42 | foreach (var property in entityType.GetProperties()) 43 | { 44 | if (dtoType.GetProperty(property.Name) == null) 45 | mappingExpression.ForMember(property.Name, opt => opt.Ignore()); 46 | } 47 | 48 | CustomMappings(mappingExpression.ReverseMap()); 49 | } 50 | 51 | public virtual void CustomMappings(IMappingExpression mapping) 52 | { 53 | } 54 | } 55 | 56 | public abstract class BaseDto : BaseDto 57 | where TDto : class, new() 58 | where TEntity : class, IEntity, new() 59 | { 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /WebFramework/Api/CrudController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using AutoMapper.QueryableExtensions; 3 | using Data.Repositories; 4 | using Entities; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.EntityFrameworkCore; 7 | using System.Collections.Generic; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace WebFramework.Api 12 | { 13 | [ApiVersion("1")] 14 | public class CrudController : BaseController 15 | where TDto : BaseDto, new() 16 | where TSelectDto : BaseDto, new() 17 | where TEntity : class, IEntity, new() 18 | { 19 | protected readonly IRepository Repository; 20 | protected readonly IMapper Mapper; 21 | 22 | public CrudController(IRepository repository, IMapper mapper) 23 | { 24 | Repository = repository; 25 | Mapper = mapper; 26 | } 27 | 28 | [HttpGet] 29 | public virtual async Task>> Get(CancellationToken cancellationToken) 30 | { 31 | var list = await Repository.TableNoTracking.ProjectTo(Mapper.ConfigurationProvider) 32 | .ToListAsync(cancellationToken); 33 | 34 | return Ok(list); 35 | } 36 | 37 | [HttpGet("{id}")] 38 | public virtual async Task> Get(TKey id, CancellationToken cancellationToken) 39 | { 40 | var dto = await Repository.TableNoTracking.ProjectTo(Mapper.ConfigurationProvider) 41 | .SingleOrDefaultAsync(p => p.Id.Equals(id), cancellationToken); 42 | 43 | if (dto == null) 44 | return NotFound(); 45 | 46 | return dto; 47 | } 48 | 49 | [HttpPost] 50 | public virtual async Task> Create(TDto dto, CancellationToken cancellationToken) 51 | { 52 | var model = dto.ToEntity(Mapper); 53 | 54 | await Repository.AddAsync(model, cancellationToken); 55 | 56 | var resultDto = await Repository.TableNoTracking.ProjectTo(Mapper.ConfigurationProvider) 57 | .SingleOrDefaultAsync(p => p.Id.Equals(model.Id), cancellationToken); 58 | 59 | return resultDto; 60 | } 61 | 62 | [HttpPut] 63 | public virtual async Task> Update(TKey id, TDto dto, CancellationToken cancellationToken) 64 | { 65 | var model = await Repository.GetByIdAsync(cancellationToken, id); 66 | 67 | model = dto.ToEntity(Mapper, model); 68 | 69 | await Repository.UpdateAsync(model, cancellationToken); 70 | 71 | var resultDto = await Repository.TableNoTracking.ProjectTo(Mapper.ConfigurationProvider) 72 | .SingleOrDefaultAsync(p => p.Id.Equals(model.Id), cancellationToken); 73 | 74 | return resultDto; 75 | } 76 | 77 | [HttpDelete("{id}")] 78 | public virtual async Task Delete(TKey id, CancellationToken cancellationToken) 79 | { 80 | var model = await Repository.GetByIdAsync(cancellationToken, id); 81 | 82 | await Repository.DeleteAsync(model, cancellationToken); 83 | 84 | return Ok(); 85 | } 86 | } 87 | 88 | public class CrudController : CrudController 89 | where TDto : BaseDto, new() 90 | where TSelectDto : BaseDto, new() 91 | where TEntity : class, IEntity, new() 92 | { 93 | public CrudController(IRepository repository, IMapper mapper) 94 | : base(repository, mapper) 95 | { 96 | } 97 | } 98 | 99 | public class CrudController : CrudController 100 | where TDto : BaseDto, new() 101 | where TEntity : class, IEntity, new() 102 | { 103 | public CrudController(IRepository repository, IMapper mapper) 104 | : base(repository, mapper) 105 | { 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /WebFramework/Configuration/ApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Common.Utilities; 2 | using Data; 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Hosting; 8 | using Services.DataInitializer; 9 | 10 | namespace WebFramework.Configuration 11 | { 12 | public static class ApplicationBuilderExtensions 13 | { 14 | public static IApplicationBuilder UseHsts(this IApplicationBuilder app, IWebHostEnvironment env) 15 | { 16 | Assert.NotNull(app, nameof(app)); 17 | Assert.NotNull(env, nameof(env)); 18 | 19 | if (!env.IsDevelopment()) 20 | app.UseHsts(); 21 | 22 | return app; 23 | } 24 | 25 | public static IApplicationBuilder IntializeDatabase(this IApplicationBuilder app) 26 | { 27 | Assert.NotNull(app, nameof(app)); 28 | 29 | //Use C# 8 using variables 30 | using var scope = app.ApplicationServices.GetRequiredService().CreateScope(); 31 | var dbContext = scope.ServiceProvider.GetService(); //Service locator 32 | 33 | //Dos not use Migrations, just Create Database with latest changes 34 | //dbContext.Database.EnsureCreated(); 35 | //Applies any pending migrations for the context to the database like (Update-Database) 36 | dbContext.Database.Migrate(); 37 | 38 | var dataInitializers = scope.ServiceProvider.GetServices(); 39 | foreach (var dataInitializer in dataInitializers) 40 | dataInitializer.InitializeData(); 41 | 42 | return app; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /WebFramework/Configuration/AutofacConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Autofac; 2 | using Common; 3 | using Data; 4 | using Data.Repositories; 5 | using Entities; 6 | using Services; 7 | 8 | namespace WebFramework.Configuration 9 | { 10 | public static class AutofacConfigurationExtensions 11 | { 12 | public static void AddServices(this ContainerBuilder containerBuilder) 13 | { 14 | //RegisterType > As > Liftetime 15 | containerBuilder.RegisterGeneric(typeof(Repository<>)).As(typeof(IRepository<>)).InstancePerLifetimeScope(); 16 | 17 | var commonAssembly = typeof(SiteSettings).Assembly; 18 | var entitiesAssembly = typeof(IEntity).Assembly; 19 | var dataAssembly = typeof(ApplicationDbContext).Assembly; 20 | var servicesAssembly = typeof(JwtService).Assembly; 21 | 22 | containerBuilder.RegisterAssemblyTypes(commonAssembly, entitiesAssembly, dataAssembly, servicesAssembly) 23 | .AssignableTo() 24 | .AsImplementedInterfaces() 25 | .InstancePerLifetimeScope(); 26 | 27 | containerBuilder.RegisterAssemblyTypes(commonAssembly, entitiesAssembly, dataAssembly, servicesAssembly) 28 | .AssignableTo() 29 | .AsImplementedInterfaces() 30 | .InstancePerDependency(); 31 | 32 | containerBuilder.RegisterAssemblyTypes(commonAssembly, entitiesAssembly, dataAssembly, servicesAssembly) 33 | .AssignableTo() 34 | .AsImplementedInterfaces() 35 | .SingleInstance(); 36 | } 37 | 38 | //We don't need this since Autofac updates for ASP.NET Core 3.0+ Generic Hosting 39 | //public static IServiceProvider BuildAutofacServiceProvider(this IServiceCollection services) 40 | //{ 41 | // var containerBuilder = new ContainerBuilder(); 42 | // containerBuilder.Populate(services); 43 | // 44 | // //Register Services to Autofac ContainerBuilder 45 | // containerBuilder.AddServices(); 46 | // 47 | // var container = containerBuilder.Build(); 48 | // return new AutofacServiceProvider(container); 49 | //} 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /WebFramework/Configuration/IdentityConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Entities; 3 | using Common; 4 | using Microsoft.AspNetCore.Identity; 5 | using Data; 6 | 7 | namespace WebFramework.Configuration 8 | { 9 | public static class IdentityConfigurationExtensions 10 | { 11 | public static void AddCustomIdentity(this IServiceCollection services, IdentitySettings settings) 12 | { 13 | services.AddIdentity(identityOptions => 14 | { 15 | //Password Settings 16 | identityOptions.Password.RequireDigit = settings.PasswordRequireDigit; 17 | identityOptions.Password.RequiredLength = settings.PasswordRequiredLength; 18 | identityOptions.Password.RequireNonAlphanumeric = settings.PasswordRequireNonAlphanumeric; //#@! 19 | identityOptions.Password.RequireUppercase = settings.PasswordRequireUppercase; 20 | identityOptions.Password.RequireLowercase = settings.PasswordRequireLowercase; 21 | 22 | //UserName Settings 23 | identityOptions.User.RequireUniqueEmail = settings.RequireUniqueEmail; 24 | 25 | //Singin Settings 26 | //identityOptions.SignIn.RequireConfirmedEmail = false; 27 | //identityOptions.SignIn.RequireConfirmedPhoneNumber = false; 28 | 29 | //Lockout Settings 30 | //identityOptions.Lockout.MaxFailedAccessAttempts = 5; 31 | //identityOptions.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); 32 | //identityOptions.Lockout.AllowedForNewUsers = false; 33 | }) 34 | .AddEntityFrameworkStores() 35 | .AddDefaultTokenProviders(); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /WebFramework/Configuration/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Common.Exceptions; 3 | using Common.Utilities; 4 | using Data; 5 | using Data.Repositories; 6 | using ElmahCore.Mvc; 7 | using ElmahCore.Sql; 8 | using Entities; 9 | using Microsoft.AspNetCore.Authentication.JwtBearer; 10 | using Microsoft.AspNetCore.Identity; 11 | using Microsoft.AspNetCore.Mvc; 12 | using Microsoft.AspNetCore.Mvc.Authorization; 13 | using Microsoft.EntityFrameworkCore; 14 | using Microsoft.Extensions.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.IdentityModel.Tokens; 17 | using Newtonsoft.Json; 18 | using Newtonsoft.Json.Converters; 19 | using System; 20 | using System.Linq; 21 | using System.Net; 22 | using System.Security.Claims; 23 | using System.Text; 24 | using System.Threading.Tasks; 25 | 26 | namespace WebFramework.Configuration 27 | { 28 | public static class ServiceCollectionExtensions 29 | { 30 | public static void AddDbContext(this IServiceCollection services, IConfiguration configuration) 31 | { 32 | services.AddDbContext(options => 33 | { 34 | options 35 | .UseSqlServer(configuration.GetConnectionString("SqlServer")); 36 | //Tips 37 | //Automatic client evaluation is no longer supported. This event is no longer generated. 38 | //This line is no longer needed. 39 | //.ConfigureWarnings(warning => warning.Throw(RelationalEventId.QueryClientEvaluationWarning)); 40 | }); 41 | } 42 | 43 | public static void AddMinimalMvc(this IServiceCollection services) 44 | { 45 | //https://github.com/aspnet/AspNetCore/blob/0303c9e90b5b48b309a78c2ec9911db1812e6bf3/src/Mvc/Mvc/src/MvcServiceCollectionExtensions.cs 46 | services.AddControllers(options => 47 | { 48 | options.Filters.Add(new AuthorizeFilter()); //Apply AuthorizeFilter as global filter to all actions 49 | 50 | //Like [ValidateAntiforgeryToken] attribute but dose not validatie for GET and HEAD http method 51 | //You can ingore validate by using [IgnoreAntiforgeryToken] attribute 52 | //Use this filter when use cookie 53 | //options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); 54 | 55 | //options.UseYeKeModelBinder(); 56 | }).AddNewtonsoftJson(option => 57 | { 58 | option.SerializerSettings.Converters.Add(new StringEnumConverter()); 59 | option.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; 60 | //option.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented; 61 | //option.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; 62 | }); 63 | services.AddSwaggerGenNewtonsoftSupport(); 64 | 65 | #region Old way (We don't need this from ASP.NET Core 3.0 onwards) 66 | ////https://github.com/aspnet/Mvc/blob/release/2.2/src/Microsoft.AspNetCore.Mvc/MvcServiceCollectionExtensions.cs 67 | //services.AddMvcCore(options => 68 | //{ 69 | // options.Filters.Add(new AuthorizeFilter()); 70 | 71 | // //Like [ValidateAntiforgeryToken] attribute but dose not validatie for GET and HEAD http method 72 | // //You can ingore validate by using [IgnoreAntiforgeryToken] attribute 73 | // //Use this filter when use cookie 74 | // //options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute()); 75 | 76 | // //options.UseYeKeModelBinder(); 77 | //}) 78 | //.AddApiExplorer() 79 | //.AddAuthorization() 80 | //.AddFormatterMappings() 81 | //.AddDataAnnotations() 82 | //.AddJsonOptions(option => 83 | //{ 84 | // //option.JsonSerializerOptions 85 | //}) 86 | //.AddNewtonsoftJson(/*option => 87 | //{ 88 | // option.SerializerSettings.Formatting = Newtonsoft.Json.Formatting.Indented; 89 | // option.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; 90 | //}*/) 91 | 92 | ////Microsoft.AspNetCore.Mvc.Formatters.Json 93 | ////.AddJsonFormatters(/*options => 94 | ////{ 95 | //// options.Formatting = Newtonsoft.Json.Formatting.Indented; 96 | //// options.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; 97 | ////}*/) 98 | 99 | //.AddCors() 100 | //.SetCompatibilityVersion(CompatibilityVersion.Latest); //.SetCompatibilityVersion(CompatibilityVersion.Version_2_1) 101 | #endregion 102 | } 103 | 104 | public static void AddElmahCore(this IServiceCollection services, IConfiguration configuration, SiteSettings siteSetting) 105 | { 106 | services.AddElmah(options => 107 | { 108 | options.Path = siteSetting.ElmahPath; 109 | options.ConnectionString = configuration.GetConnectionString("Elmah"); 110 | //options.CheckPermissionAction = httpContext => httpContext.User.Identity.IsAuthenticated; 111 | }); 112 | } 113 | 114 | public static void AddJwtAuthentication(this IServiceCollection services, JwtSettings jwtSettings) 115 | { 116 | services.AddAuthentication(options => 117 | { 118 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 119 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 120 | options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 121 | }).AddJwtBearer(options => 122 | { 123 | var secretKey = Encoding.UTF8.GetBytes(jwtSettings.SecretKey); 124 | var encryptionKey = Encoding.UTF8.GetBytes(jwtSettings.EncryptKey); 125 | 126 | var validationParameters = new TokenValidationParameters 127 | { 128 | ClockSkew = TimeSpan.Zero, // default: 5 min 129 | RequireSignedTokens = true, 130 | 131 | ValidateIssuerSigningKey = true, 132 | IssuerSigningKey = new SymmetricSecurityKey(secretKey), 133 | 134 | RequireExpirationTime = true, 135 | ValidateLifetime = true, 136 | 137 | ValidateAudience = true, //default : false 138 | ValidAudience = jwtSettings.Audience, 139 | 140 | ValidateIssuer = true, //default : false 141 | ValidIssuer = jwtSettings.Issuer, 142 | 143 | TokenDecryptionKey = new SymmetricSecurityKey(encryptionKey) 144 | }; 145 | 146 | options.RequireHttpsMetadata = false; 147 | options.SaveToken = true; 148 | options.TokenValidationParameters = validationParameters; 149 | options.Events = new JwtBearerEvents 150 | { 151 | OnAuthenticationFailed = context => 152 | { 153 | //var logger = context.HttpContext.RequestServices.GetRequiredService().CreateLogger(nameof(JwtBearerEvents)); 154 | //logger.LogError("Authentication failed.", context.Exception); 155 | 156 | if (context.Exception != null) 157 | throw new AppException(ApiResultStatusCode.UnAuthorized, "Authentication failed.", HttpStatusCode.Unauthorized, context.Exception, null); 158 | 159 | return Task.CompletedTask; 160 | }, 161 | OnTokenValidated = async context => 162 | { 163 | var signInManager = context.HttpContext.RequestServices.GetRequiredService>(); 164 | var userRepository = context.HttpContext.RequestServices.GetRequiredService(); 165 | 166 | var claimsIdentity = context.Principal.Identity as ClaimsIdentity; 167 | if (claimsIdentity.Claims?.Any() != true) 168 | context.Fail("This token has no claims."); 169 | 170 | var securityStamp = claimsIdentity.FindFirstValue(new ClaimsIdentityOptions().SecurityStampClaimType); 171 | if (!securityStamp.HasValue()) 172 | context.Fail("This token has no security stamp"); 173 | 174 | //Find user and token from database and perform your custom validation 175 | var userId = claimsIdentity.GetUserId(); 176 | var user = await userRepository.GetByIdAsync(context.HttpContext.RequestAborted, userId); 177 | 178 | //if (user.SecurityStamp != Guid.Parse(securityStamp)) 179 | // context.Fail("Token security stamp is not valid."); 180 | 181 | var validatedUser = await signInManager.ValidateSecurityStampAsync(context.Principal); 182 | if (validatedUser == null) 183 | context.Fail("Token security stamp is not valid."); 184 | 185 | if (!user.IsActive) 186 | context.Fail("User is not active."); 187 | 188 | await userRepository.UpdateLastLoginDateAsync(user, context.HttpContext.RequestAborted); 189 | }, 190 | OnChallenge = context => 191 | { 192 | //var logger = context.HttpContext.RequestServices.GetRequiredService().CreateLogger(nameof(JwtBearerEvents)); 193 | //logger.LogError("OnChallenge error", context.Error, context.ErrorDescription); 194 | 195 | if (context.AuthenticateFailure != null) 196 | throw new AppException(ApiResultStatusCode.UnAuthorized, "Authenticate failure.", HttpStatusCode.Unauthorized, context.AuthenticateFailure, null); 197 | throw new AppException(ApiResultStatusCode.UnAuthorized, "You are unauthorized to access this resource.", HttpStatusCode.Unauthorized); 198 | 199 | //return Task.CompletedTask; 200 | } 201 | }; 202 | }); 203 | } 204 | 205 | public static void AddCustomApiVersioning(this IServiceCollection services) 206 | { 207 | services.AddApiVersioning(options => 208 | { 209 | //url segment => {version} 210 | options.AssumeDefaultVersionWhenUnspecified = true; //default => false; 211 | options.DefaultApiVersion = new ApiVersion(1, 0); //v1.0 == v1 212 | options.ReportApiVersions = true; 213 | 214 | //ApiVersion.TryParse("1.0", out var version10); 215 | //ApiVersion.TryParse("1", out var version1); 216 | //var a = version10 == version1; 217 | 218 | //options.ApiVersionReader = new QueryStringApiVersionReader("api-version"); 219 | // api/posts?api-version=1 220 | 221 | //options.ApiVersionReader = new UrlSegmentApiVersionReader(); 222 | // api/v1/posts 223 | 224 | //options.ApiVersionReader = new HeaderApiVersionReader(new[] { "Api-Version" }); 225 | // header => Api-Version : 1 226 | 227 | //options.ApiVersionReader = new MediaTypeApiVersionReader() 228 | 229 | //options.ApiVersionReader = ApiVersionReader.Combine(new QueryStringApiVersionReader("api-version"), new UrlSegmentApiVersionReader()) 230 | // combine of [querystring] & [urlsegment] 231 | }); 232 | } 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /WebFramework/CustomMapping/AutoMapperConfiguration.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Linq; 5 | using System.Reflection; 6 | 7 | namespace WebFramework.CustomMapping 8 | { 9 | public static class AutoMapperConfiguration 10 | { 11 | public static void InitializeAutoMapper(this IServiceCollection services, params Assembly[] assemblies) 12 | { 13 | //With AutoMapper Instance, you need to call AddAutoMapper services and pass assemblies that contains automapper Profile class 14 | //services.AddAutoMapper(assembly1, assembly2, assembly3); 15 | //See http://docs.automapper.org/en/stable/Configuration.html 16 | //And https://code-maze.com/automapper-net-core/ 17 | 18 | services.AddAutoMapper(config => 19 | { 20 | config.AddCustomMappingProfile(); 21 | }, assemblies); 22 | 23 | #region Deprecated (Use AutoMapper Instance instead) 24 | //Mapper.Initialize(config => 25 | //{ 26 | // config.AddCustomMappingProfile(); 27 | //}); 28 | 29 | ////Compile mapping after configuration to boost map speed 30 | //Mapper.Configuration.CompileMappings(); 31 | #endregion 32 | } 33 | 34 | public static void AddCustomMappingProfile(this IMapperConfigurationExpression config) 35 | { 36 | config.AddCustomMappingProfile(Assembly.GetEntryAssembly()); 37 | } 38 | 39 | public static void AddCustomMappingProfile(this IMapperConfigurationExpression config, params Assembly[] assemblies) 40 | { 41 | var allTypes = assemblies.SelectMany(a => a.ExportedTypes); 42 | 43 | var list = allTypes.Where(type => type.IsClass && !type.IsAbstract && 44 | type.GetInterfaces().Contains(typeof(IHaveCustomMapping))) 45 | .Select(type => (IHaveCustomMapping)Activator.CreateInstance(type)); 46 | 47 | var profile = new CustomMappingProfile(list); 48 | 49 | config.AddProfile(profile); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /WebFramework/CustomMapping/CustomMappingProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using System.Collections.Generic; 3 | 4 | namespace WebFramework.CustomMapping 5 | { 6 | public class CustomMappingProfile : Profile 7 | { 8 | public CustomMappingProfile(IEnumerable haveCustomMappings) 9 | { 10 | foreach (var item in haveCustomMappings) 11 | item.CreateMappings(this); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /WebFramework/CustomMapping/IHaveCustomMapping.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | 3 | namespace WebFramework.CustomMapping 4 | { 5 | public interface IHaveCustomMapping 6 | { 7 | void CreateMappings(Profile profile); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /WebFramework/Filters/ApiResultFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | using Common; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using WebFramework.Api; 7 | 8 | namespace WebFramework.Filters 9 | { 10 | public class ApiResultFilterAttribute : ActionFilterAttribute 11 | { 12 | public override void OnResultExecuting(ResultExecutingContext context) 13 | { 14 | if (context.Result is OkObjectResult okObjectResult) 15 | { 16 | var apiResult = new ApiResult(true, ApiResultStatusCode.Success, okObjectResult.Value); 17 | context.Result = new JsonResult(apiResult) { StatusCode = okObjectResult.StatusCode }; 18 | } 19 | else if (context.Result is OkResult okResult) 20 | { 21 | var apiResult = new ApiResult(true, ApiResultStatusCode.Success); 22 | context.Result = new JsonResult(apiResult) { StatusCode = okResult.StatusCode }; 23 | } 24 | //return BadRequest() method create an ObjectResult with StatusCode 400 in recent versions, So the following code has changed a bit. 25 | else if (context.Result is ObjectResult badRequestObjectResult && badRequestObjectResult.StatusCode == 400) 26 | { 27 | string message = null; 28 | switch (badRequestObjectResult.Value) 29 | { 30 | case ValidationProblemDetails validationProblemDetails: 31 | var errorMessages = validationProblemDetails.Errors.SelectMany(p => p.Value).Distinct(); 32 | message = string.Join(" | ", errorMessages); 33 | break; 34 | case SerializableError errors: 35 | var errorMessages2 = errors.SelectMany(p => (string[])p.Value).Distinct(); 36 | message = string.Join(" | ", errorMessages2); 37 | break; 38 | case var value when value != null && !(value is ProblemDetails): 39 | message = badRequestObjectResult.Value.ToString(); 40 | break; 41 | } 42 | 43 | var apiResult = new ApiResult(false, ApiResultStatusCode.BadRequest, message); 44 | context.Result = new JsonResult(apiResult) { StatusCode = badRequestObjectResult.StatusCode }; 45 | } 46 | else if (context.Result is ObjectResult notFoundObjectResult && notFoundObjectResult.StatusCode == 404) 47 | { 48 | string message = null; 49 | if (notFoundObjectResult.Value != null && !(notFoundObjectResult.Value is ProblemDetails)) 50 | message = notFoundObjectResult.Value.ToString(); 51 | 52 | //var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound, notFoundObjectResult.Value); 53 | var apiResult = new ApiResult(false, ApiResultStatusCode.NotFound, message); 54 | context.Result = new JsonResult(apiResult) { StatusCode = notFoundObjectResult.StatusCode }; 55 | } 56 | else if (context.Result is ContentResult contentResult) 57 | { 58 | var apiResult = new ApiResult(true, ApiResultStatusCode.Success, contentResult.Content); 59 | context.Result = new JsonResult(apiResult) { StatusCode = contentResult.StatusCode }; 60 | } 61 | else if (context.Result is ObjectResult objectResult && objectResult.StatusCode == null 62 | && !(objectResult.Value is ApiResult)) 63 | { 64 | var apiResult = new ApiResult(true, ApiResultStatusCode.Success, objectResult.Value); 65 | context.Result = new JsonResult(apiResult) { StatusCode = objectResult.StatusCode }; 66 | } 67 | 68 | base.OnResultExecuting(context); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /WebFramework/Middlewares/CustomExceptionHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.IdentityModel.Tokens; 3 | using Newtonsoft.Json; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading.Tasks; 7 | using Common; 8 | using System.Net; 9 | using Common.Exceptions; 10 | using Microsoft.AspNetCore.Hosting; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.Extensions.Logging; 13 | using WebFramework.Api; 14 | using Microsoft.Extensions.Hosting; 15 | 16 | namespace WebFramework.Middlewares 17 | { 18 | public static class CustomExceptionHandlerMiddlewareExtensions 19 | { 20 | public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder) 21 | { 22 | return builder.UseMiddleware(); 23 | } 24 | } 25 | 26 | public class CustomExceptionHandlerMiddleware 27 | { 28 | private readonly RequestDelegate _next; 29 | private readonly IWebHostEnvironment _env; 30 | private readonly ILogger _logger; 31 | 32 | public CustomExceptionHandlerMiddleware(RequestDelegate next, 33 | IWebHostEnvironment env, 34 | ILogger logger) 35 | { 36 | _next = next; 37 | _env = env; 38 | _logger = logger; 39 | } 40 | 41 | public async Task Invoke(HttpContext context) 42 | { 43 | string message = null; 44 | HttpStatusCode httpStatusCode = HttpStatusCode.InternalServerError; 45 | ApiResultStatusCode apiStatusCode = ApiResultStatusCode.ServerError; 46 | 47 | try 48 | { 49 | await _next(context); 50 | } 51 | catch (AppException exception) 52 | { 53 | _logger.LogError(exception, exception.Message); 54 | httpStatusCode = exception.HttpStatusCode; 55 | apiStatusCode = exception.ApiStatusCode; 56 | 57 | if (_env.IsDevelopment()) 58 | { 59 | var dic = new Dictionary 60 | { 61 | ["Exception"] = exception.Message, 62 | ["StackTrace"] = exception.StackTrace, 63 | }; 64 | if (exception.InnerException != null) 65 | { 66 | dic.Add("InnerException.Exception", exception.InnerException.Message); 67 | dic.Add("InnerException.StackTrace", exception.InnerException.StackTrace); 68 | } 69 | if (exception.AdditionalData != null) 70 | dic.Add("AdditionalData", JsonConvert.SerializeObject(exception.AdditionalData)); 71 | 72 | message = JsonConvert.SerializeObject(dic); 73 | } 74 | else 75 | { 76 | message = exception.Message; 77 | } 78 | await WriteToResponseAsync(); 79 | } 80 | catch (SecurityTokenExpiredException exception) 81 | { 82 | _logger.LogError(exception, exception.Message); 83 | SetUnAuthorizeResponse(exception); 84 | await WriteToResponseAsync(); 85 | } 86 | catch (UnauthorizedAccessException exception) 87 | { 88 | _logger.LogError(exception, exception.Message); 89 | SetUnAuthorizeResponse(exception); 90 | await WriteToResponseAsync(); 91 | } 92 | catch (Exception exception) 93 | { 94 | _logger.LogError(exception, exception.Message); 95 | 96 | if (_env.IsDevelopment()) 97 | { 98 | var dic = new Dictionary 99 | { 100 | ["Exception"] = exception.Message, 101 | ["StackTrace"] = exception.StackTrace, 102 | }; 103 | message = JsonConvert.SerializeObject(dic); 104 | } 105 | await WriteToResponseAsync(); 106 | } 107 | 108 | async Task WriteToResponseAsync() 109 | { 110 | if (context.Response.HasStarted) 111 | throw new InvalidOperationException("The response has already started, the http status code middleware will not be executed."); 112 | 113 | var result = new ApiResult(false, apiStatusCode, message); 114 | var json = JsonConvert.SerializeObject(result); 115 | 116 | context.Response.StatusCode = (int)httpStatusCode; 117 | context.Response.ContentType = "application/json"; 118 | await context.Response.WriteAsync(json); 119 | } 120 | 121 | void SetUnAuthorizeResponse(Exception exception) 122 | { 123 | httpStatusCode = HttpStatusCode.Unauthorized; 124 | apiStatusCode = ApiResultStatusCode.UnAuthorized; 125 | 126 | if (_env.IsDevelopment()) 127 | { 128 | var dic = new Dictionary 129 | { 130 | ["Exception"] = exception.Message, 131 | ["StackTrace"] = exception.StackTrace 132 | }; 133 | if (exception is SecurityTokenExpiredException tokenException) 134 | dic.Add("Expires", tokenException.Expires.ToString()); 135 | 136 | message = JsonConvert.SerializeObject(dic); 137 | } 138 | } 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /WebFramework/Swagger/ApplySummariesOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using Common.Utilities; 2 | using Microsoft.AspNetCore.Mvc.Controllers; 3 | using Microsoft.OpenApi.Models; 4 | using Pluralize.NET; 5 | using Swashbuckle.AspNetCore.SwaggerGen; 6 | using System; 7 | using System.Linq; 8 | 9 | namespace WebFramework.Swagger 10 | { 11 | public class ApplySummariesOperationFilter : IOperationFilter 12 | { 13 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 14 | { 15 | var controllerActionDescriptor = context.ApiDescription.ActionDescriptor as ControllerActionDescriptor; 16 | if (controllerActionDescriptor == null) return; 17 | 18 | var pluralizer = new Pluralizer(); 19 | 20 | var actionName = controllerActionDescriptor.ActionName; 21 | var singularizeName = pluralizer.Singularize(controllerActionDescriptor.ControllerName); 22 | var pluralizeName = pluralizer.Pluralize(singularizeName); 23 | 24 | var parameterCount = operation.Parameters.Where(p => p.Name != "version" && p.Name != "api-version").Count(); 25 | 26 | if (IsGetAllAction()) 27 | { 28 | if (!operation.Summary.HasValue()) 29 | operation.Summary = $"Returns all {pluralizeName}"; 30 | } 31 | else if (IsActionName("Post", "Create")) 32 | { 33 | if (!operation.Summary.HasValue()) 34 | operation.Summary = $"Creates a {singularizeName}"; 35 | 36 | if (!operation.Parameters[0].Description.HasValue()) 37 | operation.Parameters[0].Description = $"A {singularizeName} representation"; 38 | } 39 | else if (IsActionName("Read", "Get")) 40 | { 41 | if (!operation.Summary.HasValue()) 42 | operation.Summary = $"Retrieves a {singularizeName} by unique id"; 43 | 44 | if (!operation.Parameters[0].Description.HasValue()) 45 | operation.Parameters[0].Description = $"a unique id for the {singularizeName}"; 46 | } 47 | else if (IsActionName("Put", "Edit", "Update")) 48 | { 49 | if (!operation.Summary.HasValue()) 50 | operation.Summary = $"Updates a {singularizeName} by unique id"; 51 | 52 | //if (!operation.Parameters[0].Description.HasValue()) 53 | // operation.Parameters[0].Description = $"A unique id for the {singularizeName}"; 54 | 55 | if (!operation.Parameters[0].Description.HasValue()) 56 | operation.Parameters[0].Description = $"A {singularizeName} representation"; 57 | } 58 | else if (IsActionName("Delete", "Remove")) 59 | { 60 | if (!operation.Summary.HasValue()) 61 | operation.Summary = $"Deletes a {singularizeName} by unique id"; 62 | 63 | if (!operation.Parameters[0].Description.HasValue()) 64 | operation.Parameters[0].Description = $"A unique id for the {singularizeName}"; 65 | } 66 | 67 | #region Local Functions 68 | bool IsGetAllAction() 69 | { 70 | foreach (var name in new[] { "Get", "Read", "Select" }) 71 | { 72 | if ((actionName.Equals(name, StringComparison.OrdinalIgnoreCase) && parameterCount == 0) || 73 | actionName.Equals($"{name}All", StringComparison.OrdinalIgnoreCase) || 74 | actionName.Equals($"{name}{pluralizeName}", StringComparison.OrdinalIgnoreCase) || 75 | actionName.Equals($"{name}All{singularizeName}", StringComparison.OrdinalIgnoreCase) || 76 | actionName.Equals($"{name}All{pluralizeName}", StringComparison.OrdinalIgnoreCase)) 77 | { 78 | return true; 79 | } 80 | } 81 | return false; 82 | } 83 | 84 | bool IsActionName(params string[] names) 85 | { 86 | foreach (var name in names) 87 | { 88 | if (actionName.Equals(name, StringComparison.OrdinalIgnoreCase) || 89 | actionName.Equals($"{name}ById", StringComparison.OrdinalIgnoreCase) || 90 | actionName.Equals($"{name}{singularizeName}", StringComparison.OrdinalIgnoreCase) || 91 | actionName.Equals($"{name}{singularizeName}ById", StringComparison.OrdinalIgnoreCase)) 92 | { 93 | return true; 94 | } 95 | } 96 | return false; 97 | } 98 | #endregion 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /WebFramework/Swagger/RemoveVersionParameters.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | using Swashbuckle.AspNetCore.SwaggerGen; 3 | using System.Linq; 4 | 5 | namespace WebFramework.Swagger 6 | { 7 | public class RemoveVersionParameters : IOperationFilter 8 | { 9 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 10 | { 11 | // Remove version parameter from all Operations 12 | var versionParameter = operation.Parameters.SingleOrDefault(p => p.Name == "version"); 13 | if (versionParameter != null) 14 | operation.Parameters.Remove(versionParameter); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /WebFramework/Swagger/SetVersionInPaths.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.OpenApi.Models; 2 | using Swashbuckle.AspNetCore.SwaggerGen; 3 | 4 | namespace WebFramework.Swagger 5 | { 6 | public class SetVersionInPaths : IDocumentFilter 7 | { 8 | public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) 9 | { 10 | var updatedPaths = new OpenApiPaths(); 11 | 12 | foreach (var entry in swaggerDoc.Paths) 13 | { 14 | updatedPaths.Add( 15 | entry.Key.Replace("v{version}", swaggerDoc.Info.Version), 16 | entry.Value); 17 | } 18 | 19 | swaggerDoc.Paths = updatedPaths; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /WebFramework/Swagger/UnauthorizedResponsesOperationFilter.cs: -------------------------------------------------------------------------------- 1 | using Swashbuckle.AspNetCore.SwaggerGen; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc.Authorization; 4 | using Microsoft.OpenApi.Models; 5 | using System; 6 | using Microsoft.AspNetCore.Authorization; 7 | 8 | namespace WebFramework.Swagger 9 | { 10 | public class UnauthorizedResponsesOperationFilter : IOperationFilter 11 | { 12 | private readonly bool includeUnauthorizedAndForbiddenResponses; 13 | private readonly string schemeName; 14 | 15 | public UnauthorizedResponsesOperationFilter(bool includeUnauthorizedAndForbiddenResponses, string schemeName = "Bearer") 16 | { 17 | this.includeUnauthorizedAndForbiddenResponses = includeUnauthorizedAndForbiddenResponses; 18 | this.schemeName = schemeName; 19 | } 20 | 21 | public void Apply(OpenApiOperation operation, OperationFilterContext context) 22 | { 23 | var filters = context.ApiDescription.ActionDescriptor.FilterDescriptors; 24 | var metadta = context.ApiDescription.ActionDescriptor.EndpointMetadata; 25 | 26 | var hasAnonymous = filters.Any(p => p.Filter is AllowAnonymousFilter) || metadta.Any(p => p is AllowAnonymousAttribute); 27 | if (hasAnonymous) return; 28 | 29 | var hasAuthorize = filters.Any(p => p.Filter is AuthorizeFilter) || metadta.Any(p => p is AuthorizeAttribute); 30 | if (!hasAuthorize) return; 31 | 32 | if (includeUnauthorizedAndForbiddenResponses) 33 | { 34 | operation.Responses.TryAdd("401", new OpenApiResponse { Description = "Unauthorized" }); 35 | operation.Responses.TryAdd("403", new OpenApiResponse { Description = "Forbidden" }); 36 | } 37 | 38 | operation.Security.Add(new OpenApiSecurityRequirement 39 | { 40 | { 41 | new OpenApiSecurityScheme 42 | { 43 | Scheme = schemeName, 44 | Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "OAuth2" } 45 | }, 46 | Array.Empty() //new[] { "readAccess", "writeAccess" } 47 | } 48 | }); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /WebFramework/WebFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 9.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------