├── .gitattributes ├── .gitignore ├── BlazingBlog.Application ├── Abstractions │ └── RequestHandler │ │ ├── ICommand.cs │ │ ├── ICommandHandler.cs │ │ ├── IQuery.cs │ │ └── IQueryHandler.cs ├── Articles │ ├── ArticleDto.cs │ ├── ArticleService.cs │ ├── ArticlesViewService.cs │ ├── CreateArticle │ │ ├── CreateArticleCommand.cs │ │ └── CreateArticleCommandHandler.cs │ ├── DeleteArticle │ │ ├── DeleteArticleCommand.cs │ │ └── DeleteArticleCommandHandler.cs │ ├── GetArticleById │ │ ├── GetArticleByIdQuery.cs │ │ └── GetArticleByIdQueryHandler.cs │ ├── GetArticleForEdit │ │ ├── GetArticleForEditQuery.cs │ │ └── GetArticleForEditQueryHandler.cs │ ├── GetArticles │ │ ├── GetArticlesQuery.cs │ │ └── GetArticlesQueryHandler.cs │ ├── GetArticlesByCurrentUser │ │ ├── GetArticlesByCurrentUserQuery.cs │ │ └── GetArticlesByCurrentUserQueryHandler.cs │ ├── IArticleService.cs │ ├── IArticlesViewService.cs │ ├── TogglePublishArticle │ │ ├── TogglePublishArticleCommand.cs │ │ └── TogglePublishArticleCommandHandler.cs │ └── UpdateArticle │ │ ├── UpdateArticleCommand.cs │ │ └── UpdateArticleCommandHandler.cs ├── Authentication │ ├── IAuthenticationService.cs │ └── RegisterUserResponse.cs ├── BlazorCleanArchitecture.Application.csproj ├── DependencyInjection.cs ├── Exceptions │ └── UserNotAuthorizedException.cs ├── GlobalImports.cs └── Users │ ├── AddRoleToUser │ ├── AddRoleToUserCommand.cs │ └── AddRoleToUserCommandHandler.cs │ ├── GetUserRoles │ ├── GetUserRolesQuery.cs │ └── GetUserRolesQueryHandler.cs │ ├── GetUsers │ ├── GetUsersQuery.cs │ └── GetUsersQueryHandler.cs │ ├── IUserService.cs │ ├── LoginUser │ ├── LoginUserCommand.cs │ └── LoginUserCommandHandler.cs │ ├── LogoutUser │ ├── LogoutUserCommand.cs │ └── LogoutUserCommandHandler.cs │ ├── RegisterUser │ ├── RegisterUserCommand.cs │ └── RegisterUserCommandHandler.cs │ ├── RemoveRoleFromUser │ ├── RemoveRoleFromUserCommand.cs │ └── RemoveUserFromRoleCommandHandler.cs │ └── UserDto.cs ├── BlazingBlog.Domain ├── Abstractions │ ├── Entity.cs │ └── Result.cs ├── Articles │ ├── Article.cs │ └── IArticleRepository.cs ├── BlazorCleanArchitecture.Domain.csproj └── Users │ ├── IUser.cs │ └── IUserRepository.cs ├── BlazingBlog.Infrastructure ├── ApplicationDbContext.cs ├── Authentication │ ├── AuthenticationService.cs │ └── AuthorizationMiddlewareResultHandler.cs ├── BlazorCleanArchitecture.Infrastructure.csproj ├── DependencyInjection.cs ├── Migrations │ ├── 20241021141617_Initial.Designer.cs │ ├── 20241021141617_Initial.cs │ ├── 20241023213136_Identity.Designer.cs │ ├── 20241023213136_Identity.cs │ └── ApplicationDbContextModelSnapshot.cs ├── Repositories │ ├── ArticleRepository.cs │ └── UserRepository.cs └── Users │ ├── User.cs │ └── UserService.cs ├── BlazingBlog.WebUI.Client ├── BlazorCleanArchitecture.WebUI.Client.csproj ├── Features │ └── Articles │ │ ├── Components │ │ └── MyArticles.razor │ │ └── MyArticlesService.cs ├── Program.cs ├── Properties │ └── launchSettings.json └── _Imports.razor ├── BlazingBlog.WebUI.Server ├── App.razor ├── BlazorCleanArchitecture.WebUI.Server.csproj ├── Features │ ├── Articles │ │ ├── ArticleModel.cs │ │ ├── Components │ │ │ ├── ArticleEditor.razor │ │ │ ├── ArticleView.razor │ │ │ └── Articles.razor │ │ └── Controllers │ │ │ └── ArticlesController.cs │ ├── UserManagement │ │ └── Components │ │ │ ├── UserRolesModal.razor │ │ │ └── Users.razor │ └── Users │ │ └── Components │ │ ├── LoginUser.razor │ │ ├── LoginUserModel.cs │ │ ├── LogoutUser.razor │ │ ├── RegisterUser.razor │ │ └── RegisterUserModel.cs ├── Layout │ ├── BlogLayout.razor │ ├── BlogLayout.razor.css │ ├── MainLayout.razor │ └── MainLayout.razor.css ├── Program.cs ├── Properties │ └── launchSettings.json ├── Routes.razor ├── Shared │ ├── Error.razor │ ├── NavMenu.razor │ ├── NavMenu.razor.css │ └── RedirectToLogin.razor ├── _Imports.razor ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── app.css │ ├── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.css.map │ └── favicon.png ├── BlazorCleanArchitecture.sln └── README.md /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /BlazingBlog.Application/Abstractions/RequestHandler/ICommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler 4 | { 5 | public interface ICommand : IRequest 6 | { 7 | } 8 | public interface ICommand : IRequest> 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Abstractions/RequestHandler/ICommandHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler 4 | { 5 | // CQRS Pattern 6 | // Command Handler for commands that do not return any specific result other than success or failure 7 | public interface ICommandHandler : IRequestHandler 8 | where TCommand : ICommand 9 | { 10 | } 11 | // CQRS Pattern 12 | // Command Handler for commands that do return a specific response type 13 | public interface ICommandHandler : IRequestHandler> 14 | where TCommand : ICommand 15 | { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Abstractions/RequestHandler/IQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler 4 | { 5 | // CQRS QUERY PATTERN 6 | // The IQuery interface inherits from the IRequest interface, meaning it can be processed by a mediator. 7 | // Returns Result which wraps both success and failure outcomes for the query. 8 | public interface IQuery : IRequest> 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Abstractions/RequestHandler/IQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | 3 | namespace BlazorCleanArchitecture.Application.Abstractions.RequestHandler 4 | { 5 | // CQRS QUERY PATTERN 6 | // Query Handler responsible for handling queries of type IQuery 7 | // Extends from IRequestHandler 8 | // Responsible for processing a query (TQuery) and returning the corresponding result (Result) 9 | // where TQuery : IQuery ensures that TQuery is a valid query (i.e., it must implement IQuery) 10 | public interface IQueryHandler : IRequestHandler> 11 | where TQuery : IQuery 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/ArticleDto.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles 2 | { 3 | public record struct ArticleDto( 4 | int Id, 5 | string Title, 6 | string? Content, 7 | DateTime DatePublished, 8 | bool IsPublished, 9 | string UserName, 10 | string UserId, 11 | bool CanEdit 12 | ) 13 | { } 14 | } 15 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/ArticleService.cs: -------------------------------------------------------------------------------- 1 | using BlazingBlog.Domain.Articles; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazingBlog.Application.Articles 9 | { 10 | public class ArticleService : IArticleService 11 | { 12 | private readonly IArticleRepository _articleRepository; 13 | public ArticleService(IArticleRepository articleRepository) 14 | { 15 | _articleRepository = articleRepository; 16 | } 17 | public async Task> GetAllArticlesAsync() 18 | { 19 | return await _articleRepository.GetAllArticlesAsync(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/ArticlesViewService.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Articles.GetArticles; 2 | using BlazorCleanArchitecture.Application.Articles.TogglePublishArticle; 3 | using MediatR; 4 | 5 | namespace BlazorCleanArchitecture.Application.Articles 6 | { 7 | public class ArticlesViewService : IArticlesViewService 8 | { 9 | private readonly ISender _sender; 10 | public ArticlesViewService(ISender sender) 11 | { 12 | _sender = sender; 13 | } 14 | public async Task?> GetArticlesByCurrentUserAsync() 15 | { 16 | var result = await _sender.Send(new GetArticlesByCurrentUserQuery()); 17 | return result; 18 | } 19 | 20 | public async Task TogglePublishArticleAsync(int articleId) 21 | { 22 | var result = await _sender.Send(new TogglePublishArticleCommand { ArticleId = articleId }); 23 | return result; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/CreateArticle/CreateArticleCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.CreateArticle 2 | { 3 | public class CreateArticleCommand : ICommand 4 | { 5 | public required string Title { get; set; } 6 | public required string Content { get; set; } 7 | public DateTime DatePublished { get; set; } = DateTime.Now; 8 | public bool IsPublished { get; set; } = false; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/CreateArticle/CreateArticleCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Exceptions; 2 | using BlazorCleanArchitecture.Application.Users; 3 | 4 | namespace BlazorCleanArchitecture.Application.Articles.CreateArticle 5 | { 6 | public class CreateArticleCommandHandler : ICommandHandler 7 | { 8 | private readonly IArticleRepository _articleRepository; 9 | private readonly IUserService _userService; 10 | public CreateArticleCommandHandler(IArticleRepository articleRepository, IUserService userService) 11 | { 12 | _articleRepository = articleRepository; 13 | _userService = userService; 14 | } 15 | public async Task> Handle(CreateArticleCommand request, CancellationToken cancellationToken) 16 | { 17 | try 18 | { 19 | var newArticle = request.Adapt
(); 20 | newArticle.UserId = await _userService.GetCurrentUserIdAsync(); 21 | if (!await _userService.CurrentUserCanCreateArticleAsync()) 22 | { 23 | return FailingResult("You are not authorized to create an article."); 24 | } 25 | var article = await _articleRepository.CreateArticleAsync(newArticle); 26 | return article.Adapt(); 27 | } 28 | catch (UserNotAuthorizedException) 29 | { 30 | return FailingResult("An error occurred creating the article."); 31 | } 32 | 33 | } 34 | private Result FailingResult(string msg) 35 | { 36 | return Result.Fail(msg ?? "Failed to create article."); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/DeleteArticle/DeleteArticleCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.DeleteArticle 2 | { 3 | public class DeleteArticleCommand : ICommand 4 | { 5 | public required int Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/DeleteArticle/DeleteArticleCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Exceptions; 2 | using BlazorCleanArchitecture.Application.Users; 3 | 4 | namespace BlazorCleanArchitecture.Application.Articles.DeleteArticle 5 | { 6 | public class DeleteArticleCommandHandler : ICommandHandler 7 | { 8 | private readonly IArticleRepository _articleRepository; 9 | private readonly IUserService _userService; 10 | public DeleteArticleCommandHandler(IArticleRepository articleRepository, IUserService userService) 11 | { 12 | _articleRepository = articleRepository; 13 | _userService = userService; 14 | } 15 | public async Task Handle(DeleteArticleCommand request, CancellationToken cancellationToken) 16 | { 17 | try 18 | { 19 | if (!await _userService.CurrentUserCanEditArticleAsync(request.Id)) 20 | { 21 | return FailingResult("You are not authorized to delete this article."); 22 | } 23 | var ok = await _articleRepository.DeleteArticleAsync(request.Id); 24 | if (ok) 25 | { 26 | return Result.OK(); 27 | } 28 | return FailingResult("Failed to delete article."); 29 | } 30 | catch (UserNotAuthorizedException) 31 | { 32 | return FailingResult("An error occurred deleting the article."); 33 | } 34 | } 35 | private Result FailingResult(string msg) 36 | { 37 | return Result.Fail(msg ?? "Failed to delete article."); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticleById/GetArticleByIdQuery.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.GetArticleById 2 | { 3 | public class GetArticleByIdQuery : IQuery 4 | { 5 | public int Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticleById/GetArticleByIdQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Articles.GetArticleById; 2 | using BlazorCleanArchitecture.Application.Users; 3 | using BlazorCleanArchitecture.Domain.Users; 4 | 5 | namespace BlazorCleanArchitecture.Application.Articles.GetArticlebyId 6 | { 7 | public class GetArticleByIdQueryHandler : IQueryHandler 8 | { 9 | private readonly IArticleRepository _articleRepository; 10 | private readonly IUserRepository _userRepository; 11 | private readonly IUserService _userService; 12 | public GetArticleByIdQueryHandler(IArticleRepository articleRepository, IUserRepository userRepository, IUserService userService) 13 | { 14 | _articleRepository = articleRepository; 15 | _userRepository = userRepository; 16 | _userService = userService; 17 | } 18 | public async Task> Handle(GetArticleByIdQuery request, CancellationToken cancellationToken) 19 | { 20 | const string _default = "Unknown"; 21 | var article = await _articleRepository.GetArticleByIdAsync(request.Id); 22 | if (article is null) 23 | { 24 | return Result.Fail("Failed to get article."); 25 | } 26 | var articleDto = article.Adapt(); 27 | if (article.UserId is not null) 28 | { 29 | var author = await _userRepository.GetUserByIdAsync(article.UserId); 30 | articleDto.UserName = author?.UserName ?? _default; 31 | articleDto.UserId = article.UserId; 32 | articleDto.CanEdit = await _userService.CurrentUserCanEditArticleAsync(article.Id); 33 | } 34 | return articleDto; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticleForEdit/GetArticleForEditQuery.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.GetArticleForEdit 2 | { 3 | public class GetArticleForEditQuery : IQuery 4 | { 5 | public int Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticleForEdit/GetArticleForEditQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Users; 2 | 3 | namespace BlazorCleanArchitecture.Application.Articles.GetArticleForEdit 4 | { 5 | public class GetArticleForEditQueryHandler : IQueryHandler 6 | { 7 | private readonly IArticleRepository _articleRepository; 8 | private readonly IUserService _userService; 9 | public GetArticleForEditQueryHandler(IArticleRepository articleRepository, IUserService userService) 10 | { 11 | _articleRepository = articleRepository; 12 | _userService = userService; 13 | } 14 | public async Task> Handle(GetArticleForEditQuery request, CancellationToken cancellationToken) 15 | { 16 | var canEdit = await _userService.CurrentUserCanEditArticleAsync(request.Id); 17 | if (!canEdit) 18 | { 19 | return Result.Fail("You are not authorized to edit this article."); 20 | } 21 | var article = await _articleRepository.GetArticleByIdAsync(request.Id); 22 | var articleDto = article.Adapt(); 23 | return articleDto; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticles/GetArticlesQuery.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.GetArticles 2 | { 3 | public class GetArticlesQuery : IQuery> 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticles/GetArticlesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Users; 2 | using BlazorCleanArchitecture.Domain.Users; 3 | 4 | namespace BlazorCleanArchitecture.Application.Articles.GetArticles 5 | { 6 | public class GetArticlesQueryHandler : IQueryHandler> 7 | { 8 | private readonly IArticleRepository _articleRepository; 9 | private readonly IUserRepository _userRepository; 10 | private readonly IUserService _userService; 11 | public GetArticlesQueryHandler(IArticleRepository articleRepository, IUserRepository userRepository, IUserService userService) 12 | { 13 | _articleRepository = articleRepository; 14 | _userRepository = userRepository; 15 | _userService = userService; 16 | } 17 | public async Task>> Handle(GetArticlesQuery request, CancellationToken cancellationToken) 18 | { 19 | const string _default = "Unknown"; 20 | var articles = await _articleRepository.GetAllArticlesAsync(); 21 | // This makes more sense, but it breaks the clean architecture 22 | //List userIds = articles.Where(a => a.UserId != null) 23 | // .Select(a => a.UserId!) 24 | // .Distinct() 25 | // .ToList(); 26 | //var users = await _userRepository.GetUsersByIdsAsync(userIds); 27 | //var userDictionary = users.ToDictionary(u => u.Id, u => u.UserName); 28 | var response = new List(); 29 | foreach (var article in articles) 30 | { 31 | var articleDto = article.Adapt(); 32 | if (article.UserId != null) 33 | { 34 | var author = await _userRepository.GetUserByIdAsync(article.UserId); 35 | articleDto.UserName = author?.UserName ?? _default; 36 | articleDto.UserId = article.UserId; 37 | articleDto.CanEdit = await _userService.CurrentUserCanEditArticleAsync(article.Id); 38 | } 39 | else 40 | { 41 | articleDto.UserName = _default; 42 | articleDto.CanEdit = false; 43 | } 44 | response.Add(articleDto); 45 | } 46 | // alternate only using IdentityManager 47 | //foreach (var article in articles) 48 | //{ 49 | // var articleDto = article.Adapt(); 50 | // if (article.UserId is not null) 51 | // { 52 | // var author = await _userRepository.GetUserByIdAsync(article.UserId); 53 | // articleDto.UserName = author?.UserName ?? _default; 54 | // } 55 | // else 56 | // { 57 | // articleDto.UserName = _default; 58 | // } 59 | // response.Add(articleDto); 60 | //} 61 | return response.OrderByDescending(e => e.DatePublished).ToList(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticlesByCurrentUser/GetArticlesByCurrentUserQuery.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.GetArticles 2 | { 3 | public class GetArticlesByCurrentUserQuery : IQuery> 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/GetArticlesByCurrentUser/GetArticlesByCurrentUserQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Users; 2 | 3 | namespace BlazorCleanArchitecture.Application.Articles.GetArticles 4 | { 5 | public class GetArticlesByCurrentUserQueryHandler : IQueryHandler> 6 | { 7 | private readonly IArticleRepository _articleRepository; 8 | private readonly IUserService _userService; 9 | public GetArticlesByCurrentUserQueryHandler(IArticleRepository articleRepository, IUserService userService) 10 | { 11 | _articleRepository = articleRepository; 12 | _userService = userService; 13 | } 14 | public async Task>> Handle(GetArticlesByCurrentUserQuery request, CancellationToken cancellationToken) 15 | { 16 | var userId = await _userService.GetCurrentUserIdAsync(); 17 | var result = await _articleRepository.GetArticlesByUserAsync(userId); 18 | var response = result.Adapt>(); 19 | return response.OrderByDescending(e => e.DatePublished).ToList(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/IArticleService.cs: -------------------------------------------------------------------------------- 1 | using BlazingBlog.Domain.Articles; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazingBlog.Application.Articles 9 | { 10 | public interface IArticleService 11 | { 12 | Task> GetAllArticlesAsync(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/IArticlesViewService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles 2 | { 3 | public interface IArticlesViewService 4 | { 5 | Task TogglePublishArticleAsync(int articleId); 6 | Task?> GetArticlesByCurrentUserAsync(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/TogglePublishArticle/TogglePublishArticleCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.TogglePublishArticle 2 | { 3 | public class TogglePublishArticleCommand : ICommand 4 | { 5 | public int ArticleId { get; set; } 6 | } 7 | } 8 | 9 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/TogglePublishArticle/TogglePublishArticleCommandHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | using BlazorCleanArchitecture.Application.Users; 3 | 4 | namespace BlazorCleanArchitecture.Application.Articles.TogglePublishArticle 5 | { 6 | public class TogglePublishArticleCommandHandler : ICommandHandler 7 | { 8 | private readonly IArticleRepository _articleRepository; 9 | private readonly IUserService _userService; 10 | 11 | public TogglePublishArticleCommandHandler(IArticleRepository articleRepository, IUserService userService) 12 | { 13 | _articleRepository = articleRepository; 14 | _userService = userService; 15 | } 16 | 17 | public async Task> Handle(TogglePublishArticleCommand request, CancellationToken cancellationToken) 18 | { 19 | if (!await _userService.CurrentUserCanEditArticleAsync(request.ArticleId)) 20 | { 21 | return Result.Fail("You are not authorized to edit this article."); 22 | } 23 | 24 | var article = await _articleRepository.GetArticleByIdAsync(request.ArticleId); 25 | if (article is null) 26 | { 27 | return Result.Fail("Article does not exist."); 28 | } 29 | 30 | article.IsPublished = !article.IsPublished; 31 | article.DateUpdated = DateTime.Now; 32 | 33 | if (article.IsPublished) 34 | { 35 | article.DatePublished = DateTime.Now; 36 | } 37 | 38 | var response = await _articleRepository.UpdateArticleAsync(article); 39 | if (response is null) 40 | { 41 | return Result.Fail("Failed to update article."); 42 | } 43 | 44 | return response.Adapt(); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/UpdateArticle/UpdateArticleCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Articles.UpdateArticle 2 | { 3 | public class UpdateArticleCommand : ICommand 4 | { 5 | public required int Id { get; set; } 6 | public required string Title { get; set; } 7 | public string? Content { get; set; } 8 | public DateTime DatePublished { get; set; } = DateTime.Now; 9 | public bool IsPublished { get; set; } = false; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Articles/UpdateArticle/UpdateArticleCommandHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Exceptions; 2 | using BlazorCleanArchitecture.Application.Users; 3 | 4 | namespace BlazorCleanArchitecture.Application.Articles.UpdateArticle 5 | { 6 | public class UpdateArticleCommandHandler : ICommandHandler 7 | { 8 | private readonly IArticleRepository _articleRepository; 9 | private readonly IUserService _userService; 10 | public UpdateArticleCommandHandler(IArticleRepository articleRepository, IUserService userService) 11 | { 12 | _articleRepository = articleRepository; 13 | _userService = userService; 14 | } 15 | public async Task> Handle(UpdateArticleCommand request, CancellationToken cancellationToken) 16 | { 17 | try 18 | { 19 | var updateArticle = request.Adapt
(); 20 | if (!await _userService.CurrentUserCanEditArticleAsync(request.Id)) 21 | { 22 | return Result.Fail("You are not authorized to edit this article."); 23 | } 24 | var result = await _articleRepository.UpdateArticleAsync(updateArticle); 25 | if (result is null) 26 | { 27 | return Result.Fail("Failed to get this article."); 28 | } 29 | return result.Adapt(); 30 | } 31 | catch (UserNotAuthorizedException) 32 | { 33 | return Result.Fail("An error occurred updating this article."); 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Authentication/IAuthenticationService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Authentication 2 | { 3 | public interface IAuthenticationService 4 | { 5 | Task RegisterUserAsync(string username, string email, string password); 6 | Task LoginUserAsync(string username, string password); 7 | Task LogoutUserAsync(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Authentication/RegisterUserResponse.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Authentication 2 | { 3 | public class RegisterUserResponse 4 | { 5 | public bool Succeeded { get; set; } 6 | public List Errors { get; set; } = new(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.Application/BlazorCleanArchitecture.Application.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /BlazingBlog.Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Articles; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace BlazorCleanArchitecture.Application 5 | { 6 | public static class DependencyInjection 7 | { 8 | public static IServiceCollection AddApplication(this IServiceCollection services) 9 | { 10 | services.AddMediatR(configuration => 11 | { 12 | configuration.RegisterServicesFromAssemblies(typeof(DependencyInjection).Assembly); 13 | }); 14 | services.AddScoped(); 15 | return services; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Exceptions/UserNotAuthorizedException.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Exceptions 2 | { 3 | public class UserNotAuthorizedException : Exception 4 | { 5 | public UserNotAuthorizedException() : base() { } 6 | public UserNotAuthorizedException(string message) : base(message) { } 7 | public UserNotAuthorizedException(string message, Exception innerException) : base(message, innerException) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazingBlog.Application/GlobalImports.cs: -------------------------------------------------------------------------------- 1 | global using BlazorCleanArchitecture.Application.Abstractions.RequestHandler; 2 | global using BlazorCleanArchitecture.Domain.Abstractions; 3 | global using BlazorCleanArchitecture.Domain.Articles; 4 | global using Mapster; 5 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/AddRoleToUser/AddRoleToUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.AddRoleToUser 2 | { 3 | public class AddRoleToUserCommand : ICommand 4 | { 5 | public required string UserId { get; set; } 6 | public required string RoleName { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/AddRoleToUser/AddRoleToUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace BlazorCleanArchitecture.Application.Users.AddRoleToUser 3 | { 4 | public class AddRoleToUserCommandHandler : ICommandHandler 5 | { 6 | private readonly IUserService _userService; 7 | public AddRoleToUserCommandHandler(IUserService userService) 8 | { 9 | _userService = userService; 10 | } 11 | public async Task Handle(AddRoleToUserCommand request, CancellationToken cancellationToken) 12 | { 13 | try 14 | { 15 | await _userService.AddRoleToUserAsync(request.UserId, request.RoleName); 16 | } 17 | catch (Exception ex) 18 | { 19 | return Result.Fail(ex.Message); 20 | } 21 | return Result.OK(); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/GetUserRoles/GetUserRolesQuery.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.GetUsers 2 | { 3 | public class GetUserRolesQuery : IQuery> 4 | { 5 | public required string UserId { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/GetUserRoles/GetUserRolesQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Users; 2 | 3 | namespace BlazorCleanArchitecture.Application.Users.GetUsers 4 | { 5 | public class GetUserRolesQueryHandler : IQueryHandler> 6 | { 7 | private readonly IUserService _userService; 8 | public GetUserRolesQueryHandler(IUserService userService, IUserRepository userRepository) 9 | { 10 | _userService = userService; 11 | } 12 | public async Task>> Handle(GetUserRolesQuery request, CancellationToken cancellationToken) 13 | { 14 | var roles = await _userService.GetUserRolesAsync(request.UserId); 15 | return Result.OK(roles); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/GetUsers/GetUsersQuery.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.GetUsers 2 | { 3 | public class GetUsersQuery : IQuery> 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/GetUsers/GetUsersQueryHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Users; 2 | 3 | namespace BlazorCleanArchitecture.Application.Users.GetUsers 4 | { 5 | public class GetUsersQueryHandler : IQueryHandler> 6 | { 7 | private readonly IUserService _userService; 8 | private readonly IUserRepository _userRepository; 9 | public GetUsersQueryHandler(IUserService userService, IUserRepository userRepository) 10 | { 11 | _userService = userService; 12 | _userRepository = userRepository; 13 | } 14 | public async Task>> Handle(GetUsersQuery request, CancellationToken cancellationToken) 15 | { 16 | if (!await _userService.IsCurrentUserInRoleAsync("Admin")) 17 | { 18 | return Result.Fail>("You are not authorized to view all users."); 19 | } 20 | var users = await _userRepository.GetAllUsersAsync(); 21 | var response = new List(); 22 | foreach (var user in users) 23 | { 24 | var userDto = user.Adapt(); 25 | userDto.Roles = string.Join(", ", await _userService.GetUserRolesAsync(user.Id)); 26 | response.Add(userDto); 27 | } 28 | return response; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/IUserService.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users 2 | { 3 | public interface IUserService 4 | { 5 | Task GetCurrentUserIdAsync(); 6 | Task IsCurrentUserInRoleAsync(string roleName); 7 | Task CurrentUserCanCreateArticleAsync(); 8 | Task CurrentUserCanEditArticleAsync(int articleId); 9 | Task> GetUserRolesAsync(string userId); 10 | Task AddRoleToUserAsync(string userId, string roleName); 11 | Task RemoveRoleFromUserAsync(string userId, string roleName); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/LoginUser/LoginUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.LoginUser 2 | { 3 | public class LoginUserCommand : ICommand 4 | { 5 | public required string UserName { get; set; } 6 | public required string Password { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/LoginUser/LoginUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | using BlazorCleanArchitecture.Application.Authentication; 3 | 4 | namespace BlazorCleanArchitecture.Application.Users.LoginUser 5 | { 6 | public class LoginUserCommandHandler : ICommandHandler 7 | { 8 | private readonly IAuthenticationService _authenticationService; 9 | public LoginUserCommandHandler(IAuthenticationService authenticationService) 10 | { 11 | _authenticationService = authenticationService; 12 | } 13 | public async Task Handle(LoginUserCommand request, CancellationToken cancellationToken) 14 | { 15 | var success = await _authenticationService.LoginUserAsync(request.UserName, request.Password); 16 | if (success) 17 | { 18 | return Result.OK(); 19 | } 20 | return Result.Fail("Invalid username or password."); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/LogoutUser/LogoutUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.LogoutUser 2 | { 3 | public class LogoutUserCommand : ICommand 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/LogoutUser/LogoutUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | using BlazorCleanArchitecture.Application.Authentication; 3 | 4 | namespace BlazorCleanArchitecture.Application.Users.LogoutUser 5 | { 6 | public class LogoutUserCommandHandler : ICommandHandler 7 | { 8 | private readonly IAuthenticationService _authenticationService; 9 | public LogoutUserCommandHandler(IAuthenticationService authenticationService) 10 | { 11 | _authenticationService = authenticationService; 12 | } 13 | public async Task Handle(LogoutUserCommand request, CancellationToken cancellationToken) 14 | { 15 | await _authenticationService.LogoutUserAsync(); 16 | return Result.OK(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/RegisterUser/RegisterUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.RegisterUser 2 | { 3 | public class RegisterUserCommand : ICommand 4 | { 5 | public required string UserName { get; set; } 6 | public required string UserEmail { get; set; } 7 | public required string Password { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/RegisterUser/RegisterUserCommandHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | using BlazorCleanArchitecture.Application.Authentication; 3 | 4 | namespace BlazorCleanArchitecture.Application.Users.RegisterUser 5 | { 6 | public class RegisterUserCommandHandler : ICommandHandler 7 | { 8 | private readonly IAuthenticationService _authenticationService; 9 | public RegisterUserCommandHandler(IAuthenticationService authenticationService) 10 | { 11 | _authenticationService = authenticationService; 12 | } 13 | public async Task Handle(RegisterUserCommand request, CancellationToken cancellationToken) 14 | { 15 | var result = await _authenticationService.RegisterUserAsync(request.UserName, request.UserEmail, request.Password); 16 | if (result.Succeeded) 17 | { 18 | return Result.OK(); 19 | } 20 | return Result.Fail($"{string.Join(", ", result.Errors)}"); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/RemoveRoleFromUser/RemoveRoleFromUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users.RemoveRoleFromUser 2 | { 3 | public class RemoveRoleFromUserCommand : ICommand 4 | { 5 | public required string UserId { get; set; } 6 | public required string RoleName { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/RemoveRoleFromUser/RemoveUserFromRoleCommandHandler.cs: -------------------------------------------------------------------------------- 1 |  2 | namespace BlazorCleanArchitecture.Application.Users.RemoveRoleFromUser 3 | { 4 | public class RemoveUserFromRoleCommandHandler : ICommandHandler 5 | { 6 | private readonly IUserService _userService; 7 | public RemoveUserFromRoleCommandHandler(IUserService userService) 8 | { 9 | _userService = userService; 10 | } 11 | 12 | public async Task Handle(RemoveRoleFromUserCommand request, CancellationToken cancellationToken) 13 | { 14 | try 15 | { 16 | await _userService.RemoveRoleFromUserAsync(request.UserId, request.RoleName); 17 | } 18 | catch (Exception ex) 19 | { 20 | return Result.Fail(ex.Message); 21 | } 22 | return Result.OK(); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /BlazingBlog.Application/Users/UserDto.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Application.Users 2 | { 3 | public record struct UserDto 4 | ( 5 | string Id, 6 | string UserName, 7 | string Email, 8 | string Roles 9 | ) 10 | { } 11 | } 12 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/Abstractions/Entity.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Domain.Abstractions 2 | { 3 | public abstract class Entity 4 | { 5 | public int Id { get; set; } 6 | public DateTime DateCreated { get; set; } 7 | public DateTime? DateUpdated { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/Abstractions/Result.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Domain.Abstractions 2 | { 3 | public class Result 4 | { 5 | public bool Success { get; set; } 6 | public bool Failure => !Success; 7 | public string? Error { get; } 8 | protected Result(bool success, string? error = null) 9 | { 10 | Success = success; 11 | Error = error; 12 | } 13 | public static Result OK() => new(true); 14 | public static Result Fail(string error) => new(false, error); 15 | public static Result OK(T value) => new(value, true, string.Empty); 16 | public static Result Fail(string error) => new(default, false, error); 17 | // Convert value to result 18 | public static Result FromValue(T? value) => value != null ? OK(value) : Fail("Provided value is null."); 19 | } 20 | public class Result : Result 21 | { 22 | public T? Value { get; } 23 | internal Result(T? value, bool ok, string error) 24 | : base(ok, error) 25 | { 26 | Value = value; 27 | } 28 | // Implicitly converts a value to a Result 29 | public static implicit operator Result(T value) => FromValue(value); 30 | // Implicitly converts a Result to a value 31 | public static implicit operator T?(Result result) => result.Value; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/Articles/Article.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Abstractions; 2 | 3 | namespace BlazorCleanArchitecture.Domain.Articles 4 | { 5 | public class Article : Entity 6 | { 7 | public required string Title { get; set; } 8 | public string? Content { get; set; } 9 | public DateTime DatePublished { get; set; } = DateTime.Now; 10 | public bool IsPublished { get; set; } = false; 11 | public string? UserId { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/Articles/IArticleRepository.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Domain.Articles 2 | { 3 | public interface IArticleRepository 4 | { 5 | Task> GetAllArticlesAsync(); 6 | Task GetArticleByIdAsync(int id); 7 | Task> GetArticlesByUserAsync(string userId); 8 | Task
CreateArticleAsync(Article article); 9 | Task UpdateArticleAsync(Article article); 10 | Task DeleteArticleAsync(int id); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/BlazorCleanArchitecture.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/Users/IUser.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Articles; 2 | 3 | namespace BlazorCleanArchitecture.Domain.Users 4 | { 5 | // Field names match Microsoft.Identity User 6 | public interface IUser 7 | { 8 | public string Id { get; set; } 9 | public string? UserName { get; set; } 10 | public string? Email { get; set; } 11 | public List
Articles { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazingBlog.Domain/Users/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.Domain.Users 2 | { 3 | public interface IUserRepository 4 | { 5 | Task GetUserByIdAsync(string userId); 6 | Task> GetUsersByIdsAsync(IEnumerable userIds); 7 | Task> GetAllUsersAsync(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Articles; 2 | using BlazorCleanArchitecture.Infrastructure.Users; 3 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace BlazorCleanArchitecture.Infrastructure 7 | { 8 | public class ApplicationDbContext : IdentityDbContext 9 | { 10 | public ApplicationDbContext(DbContextOptions options) : base(options) 11 | { 12 | } 13 | public DbSet
Articles { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Authentication/AuthenticationService.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Authentication; 2 | using BlazorCleanArchitecture.Infrastructure.Users; 3 | using Microsoft.AspNetCore.Identity; 4 | 5 | namespace BlazorCleanArchitecture.Infrastructure.Authentication 6 | { 7 | public class AuthenticationService : IAuthenticationService 8 | { 9 | private readonly UserManager _userManager; 10 | private readonly SignInManager _signInManager; 11 | public AuthenticationService(UserManager userManager, SignInManager signInManager) 12 | { 13 | _userManager = userManager; 14 | _signInManager = signInManager; 15 | } 16 | public async Task LoginUserAsync(string username, string password) 17 | { 18 | var result = await _signInManager.PasswordSignInAsync(username, password, false, false); 19 | return result.Succeeded; 20 | } 21 | 22 | public async Task LogoutUserAsync() 23 | { 24 | await _signInManager.SignOutAsync(); 25 | } 26 | 27 | public async Task RegisterUserAsync(string username, string email, string password) 28 | { 29 | var user = new User 30 | { 31 | UserName = username, 32 | Email = email, 33 | EmailConfirmed = false 34 | }; 35 | 36 | var result = await _userManager.CreateAsync(user, password); 37 | if (result.Succeeded) 38 | { 39 | await _userManager.AddToRoleAsync(user, "Reader"); 40 | } 41 | var response = new RegisterUserResponse 42 | { 43 | Succeeded = result.Succeeded, 44 | Errors = result.Errors.Select(e => e.Description).ToList() 45 | }; 46 | return response; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Authentication/AuthorizationMiddlewareResultHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authorization; 2 | using Microsoft.AspNetCore.Authorization.Policy; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace BlazorCleanArchitecture.Infrastructure.Authentication 6 | { 7 | public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler 8 | { 9 | public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) 10 | { 11 | return next(context); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/BlazorCleanArchitecture.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Authentication; 2 | using BlazorCleanArchitecture.Application.Users; 3 | using BlazorCleanArchitecture.Domain.Articles; 4 | using BlazorCleanArchitecture.Domain.Users; 5 | using BlazorCleanArchitecture.Infrastructure.Authentication; 6 | using BlazorCleanArchitecture.Infrastructure.Repositories; 7 | using BlazorCleanArchitecture.Infrastructure.Users; 8 | using Microsoft.AspNetCore.Authorization; 9 | using Microsoft.AspNetCore.Components.Authorization; 10 | using Microsoft.AspNetCore.Components.Server; 11 | using Microsoft.AspNetCore.Identity; 12 | using Microsoft.EntityFrameworkCore; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.Extensions.DependencyInjection; 15 | 16 | namespace BlazorCleanArchitecture.Infrastructure 17 | { 18 | public static class DependencyInjection 19 | { 20 | public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) 21 | { 22 | services.AddDbContext(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))); 23 | AddAuthentication(services); 24 | 25 | services.AddHttpContextAccessor(); 26 | 27 | services.AddScoped(); 28 | services.AddScoped(); 29 | services.AddScoped(); 30 | return services; 31 | } 32 | private static void AddAuthentication(IServiceCollection services) 33 | { 34 | services.AddSingleton(); 35 | services.AddScoped(); 36 | services.AddScoped(); 37 | services.AddCascadingAuthenticationState(); 38 | services.AddAuthorization(); 39 | services.AddAuthentication(options => 40 | { 41 | options.DefaultScheme = IdentityConstants.ApplicationScheme; 42 | options.DefaultSignInScheme = IdentityConstants.ExternalScheme; 43 | }).AddIdentityCookies(); 44 | services.AddIdentityCore(options => 45 | { 46 | options.SignIn.RequireConfirmedAccount = false; 47 | options.User.RequireUniqueEmail = true; 48 | }) 49 | .AddRoles() 50 | .AddEntityFrameworkStores() 51 | .AddSignInManager() 52 | .AddDefaultTokenProviders(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Migrations/20241021141617_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorCleanArchitecture.Infrastructure; 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 | #nullable disable 11 | 12 | namespace BlazorCleanArchitecture.Infrastructure.Migrations 13 | { 14 | [DbContext(typeof(ApplicationDbContext))] 15 | [Migration("20241021141617_Initial")] 16 | partial class Initial 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "8.0.10") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 25 | 26 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("BlazorCleanArchitecture.Domain.Articles.Article", b => 29 | { 30 | b.Property("Id") 31 | .ValueGeneratedOnAdd() 32 | .HasColumnType("int"); 33 | 34 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 35 | 36 | b.Property("Content") 37 | .HasColumnType("nvarchar(max)"); 38 | 39 | b.Property("DateCreated") 40 | .HasColumnType("datetime2"); 41 | 42 | b.Property("DatePublished") 43 | .HasColumnType("datetime2"); 44 | 45 | b.Property("DateUpdated") 46 | .HasColumnType("datetime2"); 47 | 48 | b.Property("IsPublished") 49 | .HasColumnType("bit"); 50 | 51 | b.Property("Title") 52 | .IsRequired() 53 | .HasColumnType("nvarchar(max)"); 54 | 55 | b.HasKey("Id"); 56 | 57 | b.ToTable("Articles"); 58 | }); 59 | #pragma warning restore 612, 618 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Migrations/20241021141617_Initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorCleanArchitecture.Infrastructure.Migrations 6 | { 7 | /// 8 | public partial class Initial : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Articles", 15 | columns: table => new 16 | { 17 | Id = table.Column(type: "int", nullable: false) 18 | .Annotation("SqlServer:Identity", "1, 1"), 19 | Title = table.Column(type: "nvarchar(max)", nullable: false), 20 | Content = table.Column(type: "nvarchar(max)", nullable: true), 21 | DatePublished = table.Column(type: "datetime2", nullable: false), 22 | IsPublished = table.Column(type: "bit", nullable: false), 23 | DateCreated = table.Column(type: "datetime2", nullable: false), 24 | DateUpdated = table.Column(type: "datetime2", nullable: true) 25 | }, 26 | constraints: table => 27 | { 28 | table.PrimaryKey("PK_Articles", x => x.Id); 29 | }); 30 | } 31 | 32 | /// 33 | protected override void Down(MigrationBuilder migrationBuilder) 34 | { 35 | migrationBuilder.DropTable( 36 | name: "Articles"); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Migrations/20241023213136_Identity.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorCleanArchitecture.Infrastructure; 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 | #nullable disable 11 | 12 | namespace BlazorCleanArchitecture.Infrastructure.Migrations 13 | { 14 | [DbContext(typeof(ApplicationDbContext))] 15 | [Migration("20241023213136_Identity")] 16 | partial class Identity 17 | { 18 | /// 19 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 20 | { 21 | #pragma warning disable 612, 618 22 | modelBuilder 23 | .HasAnnotation("ProductVersion", "8.0.10") 24 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 25 | 26 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 27 | 28 | modelBuilder.Entity("BlazorCleanArchitecture.Domain.Articles.Article", b => 29 | { 30 | b.Property("Id") 31 | .ValueGeneratedOnAdd() 32 | .HasColumnType("int"); 33 | 34 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 35 | 36 | b.Property("Content") 37 | .HasColumnType("nvarchar(max)"); 38 | 39 | b.Property("DateCreated") 40 | .HasColumnType("datetime2"); 41 | 42 | b.Property("DatePublished") 43 | .HasColumnType("datetime2"); 44 | 45 | b.Property("DateUpdated") 46 | .HasColumnType("datetime2"); 47 | 48 | b.Property("IsPublished") 49 | .HasColumnType("bit"); 50 | 51 | b.Property("Title") 52 | .IsRequired() 53 | .HasColumnType("nvarchar(max)"); 54 | 55 | b.Property("UserId") 56 | .HasColumnType("nvarchar(450)"); 57 | 58 | b.HasKey("Id"); 59 | 60 | b.HasIndex("UserId"); 61 | 62 | b.ToTable("Articles"); 63 | }); 64 | 65 | modelBuilder.Entity("BlazorCleanArchitecture.Infrastructure.Authentication.User", b => 66 | { 67 | b.Property("Id") 68 | .HasColumnType("nvarchar(450)"); 69 | 70 | b.Property("AccessFailedCount") 71 | .HasColumnType("int"); 72 | 73 | b.Property("ConcurrencyStamp") 74 | .IsConcurrencyToken() 75 | .HasColumnType("nvarchar(max)"); 76 | 77 | b.Property("Email") 78 | .HasMaxLength(256) 79 | .HasColumnType("nvarchar(256)"); 80 | 81 | b.Property("EmailConfirmed") 82 | .HasColumnType("bit"); 83 | 84 | b.Property("LockoutEnabled") 85 | .HasColumnType("bit"); 86 | 87 | b.Property("LockoutEnd") 88 | .HasColumnType("datetimeoffset"); 89 | 90 | b.Property("NormalizedEmail") 91 | .HasMaxLength(256) 92 | .HasColumnType("nvarchar(256)"); 93 | 94 | b.Property("NormalizedUserName") 95 | .HasMaxLength(256) 96 | .HasColumnType("nvarchar(256)"); 97 | 98 | b.Property("PasswordHash") 99 | .HasColumnType("nvarchar(max)"); 100 | 101 | b.Property("PhoneNumber") 102 | .HasColumnType("nvarchar(max)"); 103 | 104 | b.Property("PhoneNumberConfirmed") 105 | .HasColumnType("bit"); 106 | 107 | b.Property("SecurityStamp") 108 | .HasColumnType("nvarchar(max)"); 109 | 110 | b.Property("TwoFactorEnabled") 111 | .HasColumnType("bit"); 112 | 113 | b.Property("UserName") 114 | .HasMaxLength(256) 115 | .HasColumnType("nvarchar(256)"); 116 | 117 | b.HasKey("Id"); 118 | 119 | b.HasIndex("NormalizedEmail") 120 | .HasDatabaseName("EmailIndex"); 121 | 122 | b.HasIndex("NormalizedUserName") 123 | .IsUnique() 124 | .HasDatabaseName("UserNameIndex") 125 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 126 | 127 | b.ToTable("AspNetUsers", (string)null); 128 | }); 129 | 130 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 131 | { 132 | b.Property("Id") 133 | .HasColumnType("nvarchar(450)"); 134 | 135 | b.Property("ConcurrencyStamp") 136 | .IsConcurrencyToken() 137 | .HasColumnType("nvarchar(max)"); 138 | 139 | b.Property("Name") 140 | .HasMaxLength(256) 141 | .HasColumnType("nvarchar(256)"); 142 | 143 | b.Property("NormalizedName") 144 | .HasMaxLength(256) 145 | .HasColumnType("nvarchar(256)"); 146 | 147 | b.HasKey("Id"); 148 | 149 | b.HasIndex("NormalizedName") 150 | .IsUnique() 151 | .HasDatabaseName("RoleNameIndex") 152 | .HasFilter("[NormalizedName] IS NOT NULL"); 153 | 154 | b.ToTable("AspNetRoles", (string)null); 155 | }); 156 | 157 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 158 | { 159 | b.Property("Id") 160 | .ValueGeneratedOnAdd() 161 | .HasColumnType("int"); 162 | 163 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 164 | 165 | b.Property("ClaimType") 166 | .HasColumnType("nvarchar(max)"); 167 | 168 | b.Property("ClaimValue") 169 | .HasColumnType("nvarchar(max)"); 170 | 171 | b.Property("RoleId") 172 | .IsRequired() 173 | .HasColumnType("nvarchar(450)"); 174 | 175 | b.HasKey("Id"); 176 | 177 | b.HasIndex("RoleId"); 178 | 179 | b.ToTable("AspNetRoleClaims", (string)null); 180 | }); 181 | 182 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 183 | { 184 | b.Property("Id") 185 | .ValueGeneratedOnAdd() 186 | .HasColumnType("int"); 187 | 188 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 189 | 190 | b.Property("ClaimType") 191 | .HasColumnType("nvarchar(max)"); 192 | 193 | b.Property("ClaimValue") 194 | .HasColumnType("nvarchar(max)"); 195 | 196 | b.Property("UserId") 197 | .IsRequired() 198 | .HasColumnType("nvarchar(450)"); 199 | 200 | b.HasKey("Id"); 201 | 202 | b.HasIndex("UserId"); 203 | 204 | b.ToTable("AspNetUserClaims", (string)null); 205 | }); 206 | 207 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 208 | { 209 | b.Property("LoginProvider") 210 | .HasColumnType("nvarchar(450)"); 211 | 212 | b.Property("ProviderKey") 213 | .HasColumnType("nvarchar(450)"); 214 | 215 | b.Property("ProviderDisplayName") 216 | .HasColumnType("nvarchar(max)"); 217 | 218 | b.Property("UserId") 219 | .IsRequired() 220 | .HasColumnType("nvarchar(450)"); 221 | 222 | b.HasKey("LoginProvider", "ProviderKey"); 223 | 224 | b.HasIndex("UserId"); 225 | 226 | b.ToTable("AspNetUserLogins", (string)null); 227 | }); 228 | 229 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 230 | { 231 | b.Property("UserId") 232 | .HasColumnType("nvarchar(450)"); 233 | 234 | b.Property("RoleId") 235 | .HasColumnType("nvarchar(450)"); 236 | 237 | b.HasKey("UserId", "RoleId"); 238 | 239 | b.HasIndex("RoleId"); 240 | 241 | b.ToTable("AspNetUserRoles", (string)null); 242 | }); 243 | 244 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 245 | { 246 | b.Property("UserId") 247 | .HasColumnType("nvarchar(450)"); 248 | 249 | b.Property("LoginProvider") 250 | .HasColumnType("nvarchar(450)"); 251 | 252 | b.Property("Name") 253 | .HasColumnType("nvarchar(450)"); 254 | 255 | b.Property("Value") 256 | .HasColumnType("nvarchar(max)"); 257 | 258 | b.HasKey("UserId", "LoginProvider", "Name"); 259 | 260 | b.ToTable("AspNetUserTokens", (string)null); 261 | }); 262 | 263 | modelBuilder.Entity("BlazorCleanArchitecture.Domain.Articles.Article", b => 264 | { 265 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 266 | .WithMany("Articles") 267 | .HasForeignKey("UserId"); 268 | }); 269 | 270 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 271 | { 272 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 273 | .WithMany() 274 | .HasForeignKey("RoleId") 275 | .OnDelete(DeleteBehavior.Cascade) 276 | .IsRequired(); 277 | }); 278 | 279 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 280 | { 281 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 282 | .WithMany() 283 | .HasForeignKey("UserId") 284 | .OnDelete(DeleteBehavior.Cascade) 285 | .IsRequired(); 286 | }); 287 | 288 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 289 | { 290 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 291 | .WithMany() 292 | .HasForeignKey("UserId") 293 | .OnDelete(DeleteBehavior.Cascade) 294 | .IsRequired(); 295 | }); 296 | 297 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 298 | { 299 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 300 | .WithMany() 301 | .HasForeignKey("RoleId") 302 | .OnDelete(DeleteBehavior.Cascade) 303 | .IsRequired(); 304 | 305 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 306 | .WithMany() 307 | .HasForeignKey("UserId") 308 | .OnDelete(DeleteBehavior.Cascade) 309 | .IsRequired(); 310 | }); 311 | 312 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 313 | { 314 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 315 | .WithMany() 316 | .HasForeignKey("UserId") 317 | .OnDelete(DeleteBehavior.Cascade) 318 | .IsRequired(); 319 | }); 320 | 321 | modelBuilder.Entity("BlazorCleanArchitecture.Infrastructure.Authentication.User", b => 322 | { 323 | b.Navigation("Articles"); 324 | }); 325 | #pragma warning restore 612, 618 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Migrations/20241023213136_Identity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | #nullable disable 5 | 6 | namespace BlazorCleanArchitecture.Infrastructure.Migrations 7 | { 8 | /// 9 | public partial class Identity : Migration 10 | { 11 | /// 12 | protected override void Up(MigrationBuilder migrationBuilder) 13 | { 14 | migrationBuilder.AddColumn( 15 | name: "UserId", 16 | table: "Articles", 17 | type: "nvarchar(450)", 18 | nullable: true); 19 | 20 | migrationBuilder.CreateTable( 21 | name: "AspNetRoles", 22 | columns: table => new 23 | { 24 | Id = table.Column(type: "nvarchar(450)", nullable: false), 25 | Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 26 | NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 27 | ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) 28 | }, 29 | constraints: table => 30 | { 31 | table.PrimaryKey("PK_AspNetRoles", x => x.Id); 32 | }); 33 | 34 | migrationBuilder.CreateTable( 35 | name: "AspNetUsers", 36 | columns: table => new 37 | { 38 | Id = table.Column(type: "nvarchar(450)", nullable: false), 39 | UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 40 | NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 41 | Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 42 | NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), 43 | EmailConfirmed = table.Column(type: "bit", nullable: false), 44 | PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), 45 | SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), 46 | ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), 47 | PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), 48 | PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), 49 | TwoFactorEnabled = table.Column(type: "bit", nullable: false), 50 | LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), 51 | LockoutEnabled = table.Column(type: "bit", nullable: false), 52 | AccessFailedCount = table.Column(type: "int", nullable: false) 53 | }, 54 | constraints: table => 55 | { 56 | table.PrimaryKey("PK_AspNetUsers", x => x.Id); 57 | }); 58 | 59 | migrationBuilder.CreateTable( 60 | name: "AspNetRoleClaims", 61 | columns: table => new 62 | { 63 | Id = table.Column(type: "int", nullable: false) 64 | .Annotation("SqlServer:Identity", "1, 1"), 65 | RoleId = table.Column(type: "nvarchar(450)", nullable: false), 66 | ClaimType = table.Column(type: "nvarchar(max)", nullable: true), 67 | ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) 68 | }, 69 | constraints: table => 70 | { 71 | table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); 72 | table.ForeignKey( 73 | name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", 74 | column: x => x.RoleId, 75 | principalTable: "AspNetRoles", 76 | principalColumn: "Id", 77 | onDelete: ReferentialAction.Cascade); 78 | }); 79 | 80 | migrationBuilder.CreateTable( 81 | name: "AspNetUserClaims", 82 | columns: table => new 83 | { 84 | Id = table.Column(type: "int", nullable: false) 85 | .Annotation("SqlServer:Identity", "1, 1"), 86 | UserId = table.Column(type: "nvarchar(450)", nullable: false), 87 | ClaimType = table.Column(type: "nvarchar(max)", nullable: true), 88 | ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) 89 | }, 90 | constraints: table => 91 | { 92 | table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); 93 | table.ForeignKey( 94 | name: "FK_AspNetUserClaims_AspNetUsers_UserId", 95 | column: x => x.UserId, 96 | principalTable: "AspNetUsers", 97 | principalColumn: "Id", 98 | onDelete: ReferentialAction.Cascade); 99 | }); 100 | 101 | migrationBuilder.CreateTable( 102 | name: "AspNetUserLogins", 103 | columns: table => new 104 | { 105 | LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), 106 | ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), 107 | ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), 108 | UserId = table.Column(type: "nvarchar(450)", nullable: false) 109 | }, 110 | constraints: table => 111 | { 112 | table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); 113 | table.ForeignKey( 114 | name: "FK_AspNetUserLogins_AspNetUsers_UserId", 115 | column: x => x.UserId, 116 | principalTable: "AspNetUsers", 117 | principalColumn: "Id", 118 | onDelete: ReferentialAction.Cascade); 119 | }); 120 | 121 | migrationBuilder.CreateTable( 122 | name: "AspNetUserRoles", 123 | columns: table => new 124 | { 125 | UserId = table.Column(type: "nvarchar(450)", nullable: false), 126 | RoleId = table.Column(type: "nvarchar(450)", nullable: false) 127 | }, 128 | constraints: table => 129 | { 130 | table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); 131 | table.ForeignKey( 132 | name: "FK_AspNetUserRoles_AspNetRoles_RoleId", 133 | column: x => x.RoleId, 134 | principalTable: "AspNetRoles", 135 | principalColumn: "Id", 136 | onDelete: ReferentialAction.Cascade); 137 | table.ForeignKey( 138 | name: "FK_AspNetUserRoles_AspNetUsers_UserId", 139 | column: x => x.UserId, 140 | principalTable: "AspNetUsers", 141 | principalColumn: "Id", 142 | onDelete: ReferentialAction.Cascade); 143 | }); 144 | 145 | migrationBuilder.CreateTable( 146 | name: "AspNetUserTokens", 147 | columns: table => new 148 | { 149 | UserId = table.Column(type: "nvarchar(450)", nullable: false), 150 | LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), 151 | Name = table.Column(type: "nvarchar(450)", nullable: false), 152 | Value = table.Column(type: "nvarchar(max)", nullable: true) 153 | }, 154 | constraints: table => 155 | { 156 | table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); 157 | table.ForeignKey( 158 | name: "FK_AspNetUserTokens_AspNetUsers_UserId", 159 | column: x => x.UserId, 160 | principalTable: "AspNetUsers", 161 | principalColumn: "Id", 162 | onDelete: ReferentialAction.Cascade); 163 | }); 164 | 165 | migrationBuilder.CreateIndex( 166 | name: "IX_Articles_UserId", 167 | table: "Articles", 168 | column: "UserId"); 169 | 170 | migrationBuilder.CreateIndex( 171 | name: "IX_AspNetRoleClaims_RoleId", 172 | table: "AspNetRoleClaims", 173 | column: "RoleId"); 174 | 175 | migrationBuilder.CreateIndex( 176 | name: "RoleNameIndex", 177 | table: "AspNetRoles", 178 | column: "NormalizedName", 179 | unique: true, 180 | filter: "[NormalizedName] IS NOT NULL"); 181 | 182 | migrationBuilder.CreateIndex( 183 | name: "IX_AspNetUserClaims_UserId", 184 | table: "AspNetUserClaims", 185 | column: "UserId"); 186 | 187 | migrationBuilder.CreateIndex( 188 | name: "IX_AspNetUserLogins_UserId", 189 | table: "AspNetUserLogins", 190 | column: "UserId"); 191 | 192 | migrationBuilder.CreateIndex( 193 | name: "IX_AspNetUserRoles_RoleId", 194 | table: "AspNetUserRoles", 195 | column: "RoleId"); 196 | 197 | migrationBuilder.CreateIndex( 198 | name: "EmailIndex", 199 | table: "AspNetUsers", 200 | column: "NormalizedEmail"); 201 | 202 | migrationBuilder.CreateIndex( 203 | name: "UserNameIndex", 204 | table: "AspNetUsers", 205 | column: "NormalizedUserName", 206 | unique: true, 207 | filter: "[NormalizedUserName] IS NOT NULL"); 208 | 209 | migrationBuilder.AddForeignKey( 210 | name: "FK_Articles_AspNetUsers_UserId", 211 | table: "Articles", 212 | column: "UserId", 213 | principalTable: "AspNetUsers", 214 | principalColumn: "Id"); 215 | } 216 | 217 | /// 218 | protected override void Down(MigrationBuilder migrationBuilder) 219 | { 220 | migrationBuilder.DropForeignKey( 221 | name: "FK_Articles_AspNetUsers_UserId", 222 | table: "Articles"); 223 | 224 | migrationBuilder.DropTable( 225 | name: "AspNetRoleClaims"); 226 | 227 | migrationBuilder.DropTable( 228 | name: "AspNetUserClaims"); 229 | 230 | migrationBuilder.DropTable( 231 | name: "AspNetUserLogins"); 232 | 233 | migrationBuilder.DropTable( 234 | name: "AspNetUserRoles"); 235 | 236 | migrationBuilder.DropTable( 237 | name: "AspNetUserTokens"); 238 | 239 | migrationBuilder.DropTable( 240 | name: "AspNetRoles"); 241 | 242 | migrationBuilder.DropTable( 243 | name: "AspNetUsers"); 244 | 245 | migrationBuilder.DropIndex( 246 | name: "IX_Articles_UserId", 247 | table: "Articles"); 248 | 249 | migrationBuilder.DropColumn( 250 | name: "UserId", 251 | table: "Articles"); 252 | } 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Migrations/ApplicationDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using BlazorCleanArchitecture.Infrastructure; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | #nullable disable 10 | 11 | namespace BlazorCleanArchitecture.Infrastructure.Migrations 12 | { 13 | [DbContext(typeof(ApplicationDbContext))] 14 | partial class ApplicationDbContextModelSnapshot : ModelSnapshot 15 | { 16 | protected override void BuildModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "8.0.10") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128); 22 | 23 | SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); 24 | 25 | modelBuilder.Entity("BlazorCleanArchitecture.Domain.Articles.Article", b => 26 | { 27 | b.Property("Id") 28 | .ValueGeneratedOnAdd() 29 | .HasColumnType("int"); 30 | 31 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 32 | 33 | b.Property("Content") 34 | .HasColumnType("nvarchar(max)"); 35 | 36 | b.Property("DateCreated") 37 | .HasColumnType("datetime2"); 38 | 39 | b.Property("DatePublished") 40 | .HasColumnType("datetime2"); 41 | 42 | b.Property("DateUpdated") 43 | .HasColumnType("datetime2"); 44 | 45 | b.Property("IsPublished") 46 | .HasColumnType("bit"); 47 | 48 | b.Property("Title") 49 | .IsRequired() 50 | .HasColumnType("nvarchar(max)"); 51 | 52 | b.Property("UserId") 53 | .HasColumnType("nvarchar(450)"); 54 | 55 | b.HasKey("Id"); 56 | 57 | b.HasIndex("UserId"); 58 | 59 | b.ToTable("Articles"); 60 | }); 61 | 62 | modelBuilder.Entity("BlazorCleanArchitecture.Infrastructure.Authentication.User", b => 63 | { 64 | b.Property("Id") 65 | .HasColumnType("nvarchar(450)"); 66 | 67 | b.Property("AccessFailedCount") 68 | .HasColumnType("int"); 69 | 70 | b.Property("ConcurrencyStamp") 71 | .IsConcurrencyToken() 72 | .HasColumnType("nvarchar(max)"); 73 | 74 | b.Property("Email") 75 | .HasMaxLength(256) 76 | .HasColumnType("nvarchar(256)"); 77 | 78 | b.Property("EmailConfirmed") 79 | .HasColumnType("bit"); 80 | 81 | b.Property("LockoutEnabled") 82 | .HasColumnType("bit"); 83 | 84 | b.Property("LockoutEnd") 85 | .HasColumnType("datetimeoffset"); 86 | 87 | b.Property("NormalizedEmail") 88 | .HasMaxLength(256) 89 | .HasColumnType("nvarchar(256)"); 90 | 91 | b.Property("NormalizedUserName") 92 | .HasMaxLength(256) 93 | .HasColumnType("nvarchar(256)"); 94 | 95 | b.Property("PasswordHash") 96 | .HasColumnType("nvarchar(max)"); 97 | 98 | b.Property("PhoneNumber") 99 | .HasColumnType("nvarchar(max)"); 100 | 101 | b.Property("PhoneNumberConfirmed") 102 | .HasColumnType("bit"); 103 | 104 | b.Property("SecurityStamp") 105 | .HasColumnType("nvarchar(max)"); 106 | 107 | b.Property("TwoFactorEnabled") 108 | .HasColumnType("bit"); 109 | 110 | b.Property("UserName") 111 | .HasMaxLength(256) 112 | .HasColumnType("nvarchar(256)"); 113 | 114 | b.HasKey("Id"); 115 | 116 | b.HasIndex("NormalizedEmail") 117 | .HasDatabaseName("EmailIndex"); 118 | 119 | b.HasIndex("NormalizedUserName") 120 | .IsUnique() 121 | .HasDatabaseName("UserNameIndex") 122 | .HasFilter("[NormalizedUserName] IS NOT NULL"); 123 | 124 | b.ToTable("AspNetUsers", (string)null); 125 | }); 126 | 127 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => 128 | { 129 | b.Property("Id") 130 | .HasColumnType("nvarchar(450)"); 131 | 132 | b.Property("ConcurrencyStamp") 133 | .IsConcurrencyToken() 134 | .HasColumnType("nvarchar(max)"); 135 | 136 | b.Property("Name") 137 | .HasMaxLength(256) 138 | .HasColumnType("nvarchar(256)"); 139 | 140 | b.Property("NormalizedName") 141 | .HasMaxLength(256) 142 | .HasColumnType("nvarchar(256)"); 143 | 144 | b.HasKey("Id"); 145 | 146 | b.HasIndex("NormalizedName") 147 | .IsUnique() 148 | .HasDatabaseName("RoleNameIndex") 149 | .HasFilter("[NormalizedName] IS NOT NULL"); 150 | 151 | b.ToTable("AspNetRoles", (string)null); 152 | }); 153 | 154 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 155 | { 156 | b.Property("Id") 157 | .ValueGeneratedOnAdd() 158 | .HasColumnType("int"); 159 | 160 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 161 | 162 | b.Property("ClaimType") 163 | .HasColumnType("nvarchar(max)"); 164 | 165 | b.Property("ClaimValue") 166 | .HasColumnType("nvarchar(max)"); 167 | 168 | b.Property("RoleId") 169 | .IsRequired() 170 | .HasColumnType("nvarchar(450)"); 171 | 172 | b.HasKey("Id"); 173 | 174 | b.HasIndex("RoleId"); 175 | 176 | b.ToTable("AspNetRoleClaims", (string)null); 177 | }); 178 | 179 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 180 | { 181 | b.Property("Id") 182 | .ValueGeneratedOnAdd() 183 | .HasColumnType("int"); 184 | 185 | SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); 186 | 187 | b.Property("ClaimType") 188 | .HasColumnType("nvarchar(max)"); 189 | 190 | b.Property("ClaimValue") 191 | .HasColumnType("nvarchar(max)"); 192 | 193 | b.Property("UserId") 194 | .IsRequired() 195 | .HasColumnType("nvarchar(450)"); 196 | 197 | b.HasKey("Id"); 198 | 199 | b.HasIndex("UserId"); 200 | 201 | b.ToTable("AspNetUserClaims", (string)null); 202 | }); 203 | 204 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 205 | { 206 | b.Property("LoginProvider") 207 | .HasColumnType("nvarchar(450)"); 208 | 209 | b.Property("ProviderKey") 210 | .HasColumnType("nvarchar(450)"); 211 | 212 | b.Property("ProviderDisplayName") 213 | .HasColumnType("nvarchar(max)"); 214 | 215 | b.Property("UserId") 216 | .IsRequired() 217 | .HasColumnType("nvarchar(450)"); 218 | 219 | b.HasKey("LoginProvider", "ProviderKey"); 220 | 221 | b.HasIndex("UserId"); 222 | 223 | b.ToTable("AspNetUserLogins", (string)null); 224 | }); 225 | 226 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 227 | { 228 | b.Property("UserId") 229 | .HasColumnType("nvarchar(450)"); 230 | 231 | b.Property("RoleId") 232 | .HasColumnType("nvarchar(450)"); 233 | 234 | b.HasKey("UserId", "RoleId"); 235 | 236 | b.HasIndex("RoleId"); 237 | 238 | b.ToTable("AspNetUserRoles", (string)null); 239 | }); 240 | 241 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 242 | { 243 | b.Property("UserId") 244 | .HasColumnType("nvarchar(450)"); 245 | 246 | b.Property("LoginProvider") 247 | .HasColumnType("nvarchar(450)"); 248 | 249 | b.Property("Name") 250 | .HasColumnType("nvarchar(450)"); 251 | 252 | b.Property("Value") 253 | .HasColumnType("nvarchar(max)"); 254 | 255 | b.HasKey("UserId", "LoginProvider", "Name"); 256 | 257 | b.ToTable("AspNetUserTokens", (string)null); 258 | }); 259 | 260 | modelBuilder.Entity("BlazorCleanArchitecture.Domain.Articles.Article", b => 261 | { 262 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 263 | .WithMany("Articles") 264 | .HasForeignKey("UserId"); 265 | }); 266 | 267 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => 268 | { 269 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 270 | .WithMany() 271 | .HasForeignKey("RoleId") 272 | .OnDelete(DeleteBehavior.Cascade) 273 | .IsRequired(); 274 | }); 275 | 276 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => 277 | { 278 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 279 | .WithMany() 280 | .HasForeignKey("UserId") 281 | .OnDelete(DeleteBehavior.Cascade) 282 | .IsRequired(); 283 | }); 284 | 285 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => 286 | { 287 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 288 | .WithMany() 289 | .HasForeignKey("UserId") 290 | .OnDelete(DeleteBehavior.Cascade) 291 | .IsRequired(); 292 | }); 293 | 294 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => 295 | { 296 | b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) 297 | .WithMany() 298 | .HasForeignKey("RoleId") 299 | .OnDelete(DeleteBehavior.Cascade) 300 | .IsRequired(); 301 | 302 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 303 | .WithMany() 304 | .HasForeignKey("UserId") 305 | .OnDelete(DeleteBehavior.Cascade) 306 | .IsRequired(); 307 | }); 308 | 309 | modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => 310 | { 311 | b.HasOne("BlazorCleanArchitecture.Infrastructure.Authentication.User", null) 312 | .WithMany() 313 | .HasForeignKey("UserId") 314 | .OnDelete(DeleteBehavior.Cascade) 315 | .IsRequired(); 316 | }); 317 | 318 | modelBuilder.Entity("BlazorCleanArchitecture.Infrastructure.Authentication.User", b => 319 | { 320 | b.Navigation("Articles"); 321 | }); 322 | #pragma warning restore 612, 618 323 | } 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Repositories/ArticleRepository.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Articles; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace BlazorCleanArchitecture.Infrastructure.Repositories 5 | { 6 | public class ArticleRepository : IArticleRepository 7 | { 8 | private readonly ApplicationDbContext _context; 9 | public ArticleRepository(ApplicationDbContext context) 10 | { 11 | _context = context; 12 | } 13 | 14 | public async Task
CreateArticleAsync(Article article) 15 | { 16 | _context.Articles.Add(article); 17 | await _context.SaveChangesAsync(); 18 | return article; 19 | } 20 | 21 | public async Task DeleteArticleAsync(int id) 22 | { 23 | var deleteArticle = await GetArticleByIdAsync(id); 24 | if (deleteArticle == null) 25 | { 26 | return false; 27 | } 28 | _context.Articles.Remove(deleteArticle); 29 | await _context.SaveChangesAsync(); 30 | return true; 31 | } 32 | 33 | public async Task> GetAllArticlesAsync() 34 | { 35 | return await _context.Articles.OrderBy(e => e.Title).ToListAsync(); 36 | } 37 | 38 | public async Task GetArticleByIdAsync(int id) 39 | { 40 | var article = await _context.Articles.FindAsync(id); 41 | return article; 42 | } 43 | 44 | public async Task> GetArticlesByUserAsync(string userId) 45 | { 46 | return await _context.Articles 47 | .Where(e => e.UserId == userId) 48 | .ToListAsync(); 49 | } 50 | 51 | public async Task UpdateArticleAsync(Article article) 52 | { 53 | var updateArticle = await GetArticleByIdAsync(article.Id); 54 | if (updateArticle == null) 55 | { 56 | return null; 57 | } 58 | updateArticle.Title = article.Title; 59 | updateArticle.Content = article.Content; 60 | updateArticle.DatePublished = article.DatePublished; 61 | updateArticle.IsPublished = article.IsPublished; 62 | updateArticle.DateUpdated = DateTime.Now; 63 | await _context.SaveChangesAsync(); 64 | return updateArticle; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Users; 2 | using BlazorCleanArchitecture.Infrastructure.Users; 3 | using Microsoft.AspNetCore.Identity; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace BlazorCleanArchitecture.Infrastructure.Repositories 7 | { 8 | public class UserRepository : IUserRepository 9 | { 10 | private readonly UserManager _userManager; 11 | private readonly ApplicationDbContext _context; 12 | public UserRepository(UserManager userManager, ApplicationDbContext context) 13 | { 14 | _userManager = userManager; 15 | _context = context; 16 | } 17 | 18 | public async Task> GetAllUsersAsync() 19 | { 20 | return await _userManager.Users 21 | .Select(user => (IUser)user) 22 | .ToListAsync(); 23 | } 24 | 25 | public async Task GetUserByIdAsync(string userId) 26 | { 27 | return await _userManager.FindByIdAsync(userId); 28 | } 29 | 30 | public async Task> GetUsersByIdsAsync(IEnumerable userIds) 31 | { 32 | return await _userManager.Users 33 | .Select(user => (IUser)user) 34 | .Where(u => userIds.Contains(u.Id)) 35 | .Cast() 36 | .ToListAsync(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Users/User.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Domain.Articles; 2 | using BlazorCleanArchitecture.Domain.Users; 3 | using Microsoft.AspNetCore.Identity; 4 | 5 | namespace BlazorCleanArchitecture.Infrastructure.Users 6 | { 7 | public class User : IdentityUser, IUser 8 | { 9 | public List
Articles { get; set; } = new List
(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazingBlog.Infrastructure/Users/UserService.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Exceptions; 2 | using BlazorCleanArchitecture.Application.Users; 3 | using BlazorCleanArchitecture.Domain.Articles; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Identity; 6 | 7 | namespace BlazorCleanArchitecture.Infrastructure.Users 8 | { 9 | public class UserService : IUserService 10 | { 11 | private readonly UserManager _userManager; 12 | private readonly IHttpContextAccessor _httpContextAccessor; 13 | private readonly IArticleRepository _articleRepository; 14 | private readonly RoleManager _roleManager; 15 | 16 | public UserService( 17 | IHttpContextAccessor httpContextAccessor, 18 | UserManager userManager, 19 | IArticleRepository articleRepository, 20 | RoleManager roleManager 21 | ) 22 | { 23 | _userManager = userManager; 24 | _articleRepository = articleRepository; 25 | _httpContextAccessor = httpContextAccessor; 26 | _roleManager = roleManager; 27 | } 28 | 29 | public async Task AddRoleToUserAsync(string userId, string roleName) 30 | { 31 | var user = await _userManager.FindByIdAsync(userId); 32 | if (user is null) 33 | { 34 | throw new Exception("User not found."); 35 | } 36 | if (!await _roleManager.RoleExistsAsync(roleName)) 37 | { 38 | var roleResult = await _roleManager.CreateAsync(new IdentityRole(roleName)); 39 | if (!roleResult.Succeeded) 40 | { 41 | throw new Exception("Failed to create role."); 42 | } 43 | } 44 | var result = await _userManager.AddToRoleAsync(user, roleName); 45 | if (!result.Succeeded) 46 | { 47 | throw new Exception("Failed to add user role."); 48 | } 49 | } 50 | 51 | public async Task CurrentUserCanCreateArticleAsync() 52 | { 53 | var user = await GetCurrentUserAsync(); 54 | if (user is null) 55 | { 56 | throw new UserNotAuthorizedException(); 57 | } 58 | var isAdmin = await _userManager.IsInRoleAsync(user, "Admin"); 59 | var isWriter = await _userManager.IsInRoleAsync(user, "Writer"); 60 | return isAdmin || isWriter; 61 | } 62 | 63 | public async Task CurrentUserCanEditArticleAsync(int articleId) 64 | { 65 | var user = await GetCurrentUserAsync(); 66 | if (user is null) 67 | { 68 | return false; 69 | } 70 | var isAdmin = await _userManager.IsInRoleAsync(user, "Admin"); 71 | var isWriter = await _userManager.IsInRoleAsync(user, "Writer"); 72 | var article = await _articleRepository.GetArticleByIdAsync(articleId); 73 | if (article is null) 74 | { 75 | return false; 76 | } 77 | return isAdmin || (isWriter && user.Id == article.UserId); 78 | } 79 | 80 | public async Task GetCurrentUserIdAsync() 81 | { 82 | var user = await GetCurrentUserAsync(); 83 | if (user is null) 84 | { 85 | throw new UserNotAuthorizedException(); 86 | } 87 | return user.Id; 88 | } 89 | 90 | public async Task> GetUserRolesAsync(string userId) 91 | { 92 | var user = await _userManager.FindByIdAsync(userId); 93 | if (user is null) 94 | { 95 | return []; 96 | } 97 | var roles = await _userManager.GetRolesAsync(user); 98 | return roles.ToList(); 99 | } 100 | 101 | public async Task IsCurrentUserInRoleAsync(string role) 102 | { 103 | var user = await GetCurrentUserAsync(); 104 | var result = user is not null && await _userManager.IsInRoleAsync(user, role); 105 | return result; 106 | } 107 | 108 | public async Task RemoveRoleFromUserAsync(string userId, string roleName) 109 | { 110 | var user = await _userManager.FindByIdAsync(userId); 111 | if (user is null) 112 | { 113 | throw new Exception("User not found."); 114 | } 115 | var result = await _userManager.RemoveFromRoleAsync(user, roleName); 116 | if (!result.Succeeded) 117 | { 118 | throw new Exception("Failed to remove user role."); 119 | } 120 | } 121 | 122 | private async Task GetCurrentUserAsync() 123 | { 124 | var httpContext = _httpContextAccessor.HttpContext; 125 | if (httpContext is null || httpContext.User is null) 126 | { 127 | return null; 128 | } 129 | return await _userManager.GetUserAsync(httpContext.User); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Client/BlazorCleanArchitecture.WebUI.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Client/Features/Articles/Components/MyArticles.razor: -------------------------------------------------------------------------------- 1 | @page "/my-articles" 2 | @rendermode @(new InteractiveAutoRenderMode(true)) 3 | @inject NavigationManager NavigationManager 4 | @inject IArticlesViewService ArticlesViewService 5 | @attribute [Authorize] 6 | 7 |

My Articles

8 | My Articles 9 | 10 | @if (!string.IsNullOrEmpty(errorMessage)) 11 | { 12 |

@errorMessage

13 | } 14 | @if (articles is null) 15 | { 16 | Loading articles... 17 | } 18 | else if (articles.Count == 0) 19 | { 20 | No articles were found. 21 | } 22 | else 23 | { 24 | 25 | 30 | 31 | @if(context.IsPublished) 32 | { 33 | 34 | } 35 | else 36 | { 37 | 38 | } 39 | 40 | 45 | 46 | 52 | 53 | 54 | 59 | 60 | 61 | } 62 | 63 | @code { 64 | private List? articles; 65 | private string errorMessage = string.Empty; 66 | protected override async Task OnInitializedAsync() 67 | { 68 | articles = await ArticlesViewService.GetArticlesByCurrentUserAsync(); 69 | if (articles is null) 70 | { 71 | articles = []; 72 | } 73 | } 74 | private void EditArticle(int id) 75 | { 76 | NavigationManager.NavigateTo($"/article-editor/{id}"); 77 | } 78 | private async Task TogglePublishArticle(int id) 79 | { 80 | Console.Write("TogglePublishArticle", id); 81 | var updatedArticle = await ArticlesViewService.TogglePublishArticleAsync(id); 82 | if (updatedArticle is null || articles is null) 83 | { 84 | errorMessage = "Failed to toggle article publication."; 85 | return; 86 | } 87 | var index = articles!.FindIndex(a => a.Id == id); 88 | if (index != -1) 89 | { 90 | articles[index] = (ArticleDto)updatedArticle; 91 | // Explicit refresh 92 | // StateHasChanged(); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Client/Features/Articles/MyArticlesService.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Articles; 2 | using System.Net.Http.Json; 3 | 4 | namespace BlazorCleanArchitecture.WebUI.Client.Features.Articles 5 | { 6 | public class MyArticlesService : IArticlesViewService 7 | { 8 | private readonly HttpClient _http; 9 | public MyArticlesService(HttpClient http) 10 | { 11 | _http = http; 12 | } 13 | public async Task?> GetArticlesByCurrentUserAsync() 14 | { 15 | return await _http.GetFromJsonAsync>("/api/Articles"); 16 | } 17 | 18 | public async Task TogglePublishArticleAsync(int articleId) 19 | { 20 | var result = await _http.PatchAsync($"api/Articles/{articleId}", null); 21 | if (result is not null && result.Content is not null) 22 | { 23 | return await result.Content.ReadFromJsonAsync(); 24 | } 25 | return null; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Articles; 2 | using BlazorCleanArchitecture.WebUI.Client.Features.Articles; 3 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 4 | 5 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 6 | 7 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 8 | 9 | builder.Services.AddScoped(); 10 | 11 | await builder.Build().RunAsync(); 12 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "BlazorCleanArchitecture.WebUI.Client": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "https://localhost:55348;http://localhost:55349" 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using BlazorCleanArchitecture.WebUI.Client 10 | @using BlazorCleanArchitecture.Application.Articles 11 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 12 | @using Microsoft.AspNetCore.Components.QuickGrid 13 | @using Microsoft.AspNetCore.Authorization 14 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/App.razor: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/BlazorCleanArchitecture.WebUI.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Articles/ArticleModel.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorCleanArchitecture.WebUI.Server.Features.Articles 2 | { 3 | public class ArticleModel 4 | { 5 | public int Id { get; set; } 6 | public string Title { get; set; } = string.Empty; 7 | public string? Content { get; set; } 8 | public DateTime DatePublished { get; set; } = DateTime.Now; 9 | public bool IsPublished { get; set; } = false; 10 | public string? UserName { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Articles/Components/ArticleEditor.razor: -------------------------------------------------------------------------------- 1 | @page "/article-editor" 2 | @page "/article-editor/{ArticleId:int}" 3 | @inject ISender Sender 4 | @inject NavigationManager NavigationManager 5 | @attribute [StreamRendering] 6 | @attribute [Authorize(Roles = "Admin, Writer")] 7 | @using BlazorCleanArchitecture.Application.Articles.GetArticleForEdit 8 | @using BlazorCleanArchitecture.Infrastructure.Users 9 | @PageTitle 10 |
11 |

@PageTitle

12 | 13 | 14 | @if (Article is null) 15 | { 16 |

Loading...

17 | } 18 | else 19 | { 20 | 21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 | 29 |
30 |
31 | 32 | 33 |
34 |
35 | 36 | 37 |
38 | 39 |
40 | 41 | @if (isEditMode) 42 | { 43 |
44 |

Danger Zone

45 | 46 | 47 | 48 | } 49 | } 50 | @errorMessage 51 |
52 | 53 | Back it on up. 54 | 55 |
56 |
57 | 58 | @code { 59 | private bool isEditMode => ArticleId is not null; 60 | private string errorMessage = string.Empty; 61 | private string PageTitle => 62 | !string.IsNullOrEmpty(errorMessage) ? "Error" : (isEditMode ? $"Edit \"{(Article?.Title ?? "Article")}\"" : "Create New Article"); 63 | 64 | [SupplyParameterFromForm] 65 | ArticleModel? Article { get; set; } 66 | [Parameter] 67 | public int? ArticleId { get; set; } 68 | [CascadingParameter] 69 | private HttpContext HttpContext { get; set; } = default!; 70 | protected async override Task OnParametersSetAsync() 71 | { 72 | if (ArticleId is not null) 73 | { 74 | var result = await Sender.Send(new GetArticleForEditQuery { Id = (int)ArticleId }); 75 | if (result.Success) 76 | { 77 | Article ??= result.Value.Adapt(); 78 | } 79 | else 80 | { 81 | SetErrorMessage(result.Error); 82 | } 83 | } 84 | else 85 | { 86 | Article ??= new(); 87 | } 88 | } 89 | async Task HandleSubmit() 90 | { 91 | if (isEditMode) 92 | { 93 | var command = Article.Adapt(); 94 | var result = await Sender.Send(command); 95 | if (result.Success) 96 | { 97 | Article = result.Value.Adapt(); 98 | } 99 | else 100 | { 101 | SetErrorMessage(result.Error); 102 | } 103 | } 104 | else 105 | { 106 | var command = Article.Adapt(); 107 | var result = await Sender.Send(command); 108 | if (result.Success) 109 | { 110 | NavigationManager.NavigateTo($"/article-editor/{result.Value.Id}"); 111 | } 112 | else 113 | { 114 | SetErrorMessage(result.Error); 115 | } 116 | } 117 | } 118 | async Task DeleteArticle() 119 | { 120 | if (ArticleId is null) 121 | { 122 | return; 123 | } 124 | var command = new DeleteArticleCommand { Id = (int)ArticleId }; 125 | var result = await Sender.Send(command); 126 | if (result.Success) 127 | { 128 | NavigationManager.NavigateTo("/"); 129 | } 130 | else 131 | { 132 | SetErrorMessage(result.Error); 133 | } 134 | } 135 | void SetErrorMessage(string? error) 136 | { 137 | errorMessage = error ?? string.Empty; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Articles/Components/ArticleView.razor: -------------------------------------------------------------------------------- 1 | @page "/articles/{ArticleId:int}" 2 | @inject ISender Sender 3 | @attribute [StreamRendering] 4 | 5 | @article.Title 6 | 7 |
8 |

@article.Title

9 | @if (!string.IsNullOrEmpty(errorMessage)) 10 | { 11 |

Error

12 |

@errorMessage

13 | 14 | 15 | by @article.UserName 16 | @if (article.CanEdit) 17 | { 18 | 19 | | Edit Article 20 | 21 | } 22 | 23 |

24 | @article.Title 25 |

26 |

@article.Content

27 | } 28 |
29 | 30 | @code { 31 | [Parameter] 32 | public int ArticleId { get; set; } 33 | private ArticleDto article; 34 | private string errorMessage = string.Empty; 35 | 36 | protected override async Task OnParametersSetAsync() 37 | { 38 | var result = await Sender.Send(new GetArticleByIdQuery { Id = (int)ArticleId }); 39 | if (result.Success && result.Value is not null) 40 | { 41 | article = (ArticleDto)result.Value; 42 | } 43 | else 44 | { 45 | errorMessage = result.Error ?? "Sorry, something went wrong."; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Articles/Components/Articles.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using BlazorCleanArchitecture.Application.Articles 3 | @using BlazorCleanArchitecture.Application.Articles.GetArticles 4 | @using MediatR 5 | @inject ISender Sender 6 | @attribute [StreamRendering] 7 | 8 | Articles 9 |

Articles

10 | 11 | Create Article 12 | 13 |
14 | @if (articles is null) 15 | { 16 |

Loading articles...

17 | } 18 | else if (!string.IsNullOrEmpty(errorMessage)) 19 | { 20 |

@errorMessage

21 | } 22 | else if (articles.Count <= 0) 23 | { 24 |

No articles could be found.

25 | } 26 | else 27 | { 28 |
    29 | @foreach (var article in articles) 30 | { 31 |
  • 32 |
    33 |
    34 |
    Published on
    35 |
    36 | 37 | by @article.UserName 38 | @if (article.CanEdit) 39 | { 40 | 41 | | Edit 42 | 43 | } 44 |
    45 |
    46 |
    47 |

    48 | 49 | @article.Title 50 | @article.Title 51 | 52 |

    53 |

    @article.Content

    54 |
    55 |
    56 |
  • 57 | } 58 |
59 | } 60 |
61 | 62 | @code { 63 | private List? articles; 64 | private string errorMessage = string.Empty; 65 | private const string _err = "Failed to get articles."; 66 | protected override async Task OnInitializedAsync() 67 | { 68 | // Delay 69 | // await Task.Delay(500); 70 | 71 | // Get 72 | var result = await Sender.Send(new GetArticlesQuery()); 73 | 74 | // Set 75 | if (result.Success) 76 | { 77 | articles = result; 78 | } 79 | else 80 | { 81 | errorMessage = result.Error ?? _err; 82 | articles = new(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Articles/Controllers/ArticlesController.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application.Articles; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace BlazorCleanArchitecture.WebUI.Server.Features.Articles.Controllers 5 | { 6 | [Route("api/[controller]")] 7 | [ApiController] 8 | public class ArticlesController : ControllerBase 9 | { 10 | private readonly IArticlesViewService _articlesViewService; 11 | public ArticlesController(IArticlesViewService articlesViewService) 12 | { 13 | _articlesViewService = articlesViewService; 14 | } 15 | [HttpGet] 16 | public async Task>> GetArticlesByCurrentUser() 17 | { 18 | var result = await _articlesViewService.GetArticlesByCurrentUserAsync(); 19 | if (result is null) 20 | { 21 | return StatusCode(500, "Failed to get user articles."); 22 | 23 | } 24 | return Ok(result); 25 | } 26 | [HttpPatch("{id}")] 27 | public async Task> TogglePublishArticle(int id) 28 | { 29 | var result = await _articlesViewService.TogglePublishArticleAsync(id); 30 | if (result is null) 31 | { 32 | return StatusCode(500, "Failed to toggle article publication."); 33 | 34 | } 35 | return Ok(result); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/UserManagement/Components/UserRolesModal.razor: -------------------------------------------------------------------------------- 1 | @using BlazorCleanArchitecture.Application.Users.AddRoleToUser 2 | @using BlazorCleanArchitecture.Application.Users.RemoveRoleFromUser 3 | @inject ISender Sender 4 | @if (ShowModal) 5 | { 6 |
7 |
8 |

9 | 10 | 11 | 12 | 13 | Change Roles for @UserName 14 |

15 |

16 | 17 | 18 | 19 | 24 | 25 | 26 |

27 |
28 | 29 | 30 |
31 |
32 | 35 |
36 |
37 |
38 | } 39 | 40 | @code { 41 | [Parameter] 42 | public bool ShowModal { get; set; } 43 | [Parameter] 44 | public EventCallback ModalClosed { get; set; } 45 | [Parameter] 46 | public required string UserId { get; set; } 47 | [Parameter] 48 | public string? UserName { get; set; } 49 | private List Roles { get; set; } = []; 50 | private string newRole = string.Empty; 51 | 52 | protected override async Task OnParametersSetAsync() 53 | { 54 | await LoadUserRoles(); 55 | } 56 | private async Task AddRole() 57 | { 58 | if (!string.IsNullOrWhiteSpace(newRole)) 59 | { 60 | await Sender.Send(new AddRoleToUserCommand { UserId = UserId, RoleName = newRole } ); 61 | await LoadUserRoles(); 62 | newRole = string.Empty; 63 | } 64 | } 65 | private async Task RemoveRole(string roleName) 66 | { 67 | await Sender.Send(new RemoveRoleFromUserCommand { UserId = UserId, RoleName = roleName }); 68 | await LoadUserRoles(); 69 | } 70 | private void CloseModal() 71 | { 72 | ShowModal = false; 73 | newRole = string.Empty; 74 | ModalClosed.InvokeAsync(ShowModal); 75 | } 76 | private async Task LoadUserRoles() 77 | { 78 | if (ShowModal && UserId is not null) 79 | { 80 | Roles = (await Sender.Send(new GetUserRolesQuery { UserId = UserId } )).Value ?? []; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/UserManagement/Components/Users.razor: -------------------------------------------------------------------------------- 1 | @page "/users" 2 | @rendermode @(new InteractiveServerRenderMode(false)) 3 | @attribute [Authorize(Roles = "Admin")] 4 | @inject ISender Sender 5 | 6 | Users 7 |

Registered Users

8 |
9 | @if (users is null) 10 | { 11 |

Loading users...

12 | } 13 | else if (!string.IsNullOrEmpty(errorMessage)) 14 | { 15 |

@errorMessage

16 | } 17 | else if (users.Count <= 0) 18 | { 19 |

No users could be found.

20 | } 21 | else 22 | { 23 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | } 37 | 38 |
39 | @code { 40 | private List? users; 41 | private bool showModal = false; 42 | private string selectedUserId = string.Empty; 43 | private string selectedUserName = string.Empty; 44 | private string errorMessage = string.Empty; 45 | private string _err = "Failed to get users."; 46 | protected override async Task OnInitializedAsync() 47 | { 48 | // Delay 49 | // await Task.Delay(2000); 50 | await LoadUsers(); 51 | } 52 | private async Task LoadUsers() 53 | { 54 | var result = await Sender.Send(new GetUsersQuery()); 55 | if (result.Success) 56 | { 57 | users = result.Value!; 58 | } 59 | else 60 | { 61 | users = new(); 62 | errorMessage = result.Error ?? _err; 63 | } 64 | } 65 | private void OpenModal(string userId, string userName) 66 | { 67 | selectedUserId = userId; 68 | selectedUserName = userName; 69 | showModal = true; 70 | Console.WriteLine($"Open Modal -> {selectedUserName} // {selectedUserId}"); 71 | } 72 | private async Task CloseModal() 73 | { 74 | showModal = false; 75 | await LoadUsers(); 76 | // if above didn't make component rerender 77 | // this statement explicitly will 78 | // but we don't need it 79 | // StateHasChanged(); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Users/Components/LoginUser.razor: -------------------------------------------------------------------------------- 1 | @page "/login" 2 | @using BlazorCleanArchitecture.Application.Users.LoginUser 3 | @inject ISender Sender 4 | @inject NavigationManager NavigationManager 5 | 6 | Login 7 |
8 |

Login

9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 | 22 |
23 | @errorMessage 24 |
25 | 26 | @code { 27 | [SupplyParameterFromForm] 28 | private LoginUserModel UserModel { get; set; } = new(); 29 | private string errorMessage = string.Empty; 30 | async Task HandleSubmit() 31 | { 32 | var command = new LoginUserCommand 33 | { 34 | UserName = UserModel.UserName, 35 | Password = UserModel.Password 36 | }; 37 | var result = await Sender.Send(command); 38 | if (result.Success) 39 | { 40 | NavigationManager.NavigateTo("/"); 41 | } 42 | else 43 | { 44 | errorMessage = result.Error ?? "Login failed."; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Users/Components/LoginUserModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace BlazorCleanArchitecture.WebUI.Server.Features.Users.Component 4 | { 5 | public class LoginUserModel 6 | { 7 | [Required] 8 | [Display(Name = "Username")] 9 | public string UserName { get; set; } = string.Empty; 10 | 11 | [Required] 12 | [DataType(DataType.Password)] 13 | [Display(Name = "Password")] 14 | public string Password { get; set; } = string.Empty; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Users/Components/LogoutUser.razor: -------------------------------------------------------------------------------- 1 | @page "/logout" 2 | @using BlazorCleanArchitecture.Application.Users.LogoutUser 3 | @inject ISender Sender 4 | @inject NavigationManager NavigationManager 5 | 6 |

Logging out...

7 | 8 | @code { 9 | protected override async Task OnInitializedAsync() 10 | { 11 | var command = new LogoutUserCommand(); 12 | await Sender.Send(command); 13 | NavigationManager.NavigateTo("/"); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Users/Components/RegisterUser.razor: -------------------------------------------------------------------------------- 1 | @page "/register" 2 | @using BlazorCleanArchitecture.Application.Users.RegisterUser 3 | @inject ISender Sender 4 | @inject NavigationManager NavigationManager 5 | 6 | Create Account 7 |
8 |

Create Account

9 | 10 | 11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | 25 |
26 |
27 | 28 | 29 | 30 |
31 | 32 |
33 | @errorMessage 34 |
35 | 36 | @code { 37 | [SupplyParameterFromForm] 38 | private RegisterUserModel UserModel { get; set; } = new(); 39 | private string errorMessage = string.Empty; 40 | async Task HandleSubmit() 41 | { 42 | var command = new RegisterUserCommand 43 | { 44 | UserName = UserModel.UserName, 45 | UserEmail = UserModel.Email, 46 | Password = UserModel.Password 47 | }; 48 | var result = await Sender.Send(command); 49 | if (result.Success) 50 | { 51 | NavigationManager.NavigateTo("/login"); 52 | } 53 | else 54 | { 55 | errorMessage = result.Error ?? "Registration failed."; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Features/Users/Components/RegisterUserModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace BlazorCleanArchitecture.WebUI.Server.Features.Users.Component 4 | { 5 | public class RegisterUserModel 6 | { 7 | [Required] 8 | [Display(Name = "Username")] 9 | public string UserName { get; set; } = string.Empty; 10 | 11 | [Required] 12 | [EmailAddress] 13 | [Display(Name = "Email Address")] 14 | public string Email { get; set; } = string.Empty; 15 | 16 | [Required] 17 | [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] 18 | [DataType(DataType.Password)] 19 | [Display(Name = "Password")] 20 | public string Password { get; set; } = string.Empty; 21 | 22 | [Required] 23 | [DataType(DataType.Password)] 24 | [Display(Name = "Confirm password")] 25 | [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] 26 | public string ConfirmPassword { get; set; } = string.Empty; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Layout/BlogLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 |
3 |
4 |
5 | My Blog 6 |
7 | 26 | 27 |
28 |
29 |
30 | @Body 31 |
32 |
33 |
34 |
35 | An unhandled error has occurred. 36 | Reload 37 | 🗙 38 |
39 | 40 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Layout/BlogLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | 19 |
20 | An unhandled error has occurred. 21 | Reload 22 | 🗙 23 |
24 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorCleanArchitecture.Application; 2 | using BlazorCleanArchitecture.Infrastructure; 3 | using BlazorCleanArchitecture.WebUI.Server; 4 | var builder = WebApplication.CreateBuilder(args); 5 | 6 | // Add services to the container. 7 | builder.Services.AddRazorComponents() 8 | .AddInteractiveServerComponents() 9 | .AddInteractiveWebAssemblyComponents(); 10 | 11 | // Dependency Injection 12 | builder.Services.AddApplication(); 13 | builder.Services.AddInfrastructure(builder.Configuration); 14 | 15 | builder.Services.AddControllers(); 16 | 17 | // Build App 18 | var app = builder.Build(); 19 | 20 | // Configure the HTTP request pipeline. 21 | if (!app.Environment.IsDevelopment()) 22 | { 23 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 24 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 25 | app.UseHsts(); 26 | } 27 | 28 | app.UseHttpsRedirection(); 29 | 30 | app.UseStaticFiles(); 31 | app.UseAntiforgery(); 32 | 33 | app.MapRazorComponents() 34 | .AddInteractiveServerRenderMode() 35 | .AddInteractiveWebAssemblyRenderMode() 36 | .AddAdditionalAssemblies(typeof(BlazorCleanArchitecture.WebUI.Client.Features.Articles.Components.MyArticles).Assembly); 37 | 38 | app.MapControllers(); 39 | 40 | app.Run(); 41 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:35984", 8 | "sslPort": 44301 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5166", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7146;http://localhost:5166", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Routes.razor: -------------------------------------------------------------------------------- 1 | @using Microsoft.AspNetCore.Identity 2 | @inject Microsoft.AspNetCore.Http.IHttpContextAccessor HttpContextAccessor 3 | 4 | 5 | 6 | 7 | 8 |

Authorizing...

9 |
10 | 11 | @if (HttpContextAccessor.HttpContext?.User?.Identity?.IsAuthenticated ?? false) 12 | { 13 |

You are not authorized to view this page.

14 | Login 15 | } 16 | else 17 | { 18 | 19 | } 20 |
21 |
22 | 23 |
24 |
25 | 26 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Shared/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

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

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  6 | 7 | 8 | 9 | 49 | 50 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Shared/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep .nav-link { 63 | color: #d7d7d7; 64 | background: none; 65 | border: none; 66 | border-radius: 4px; 67 | height: 3rem; 68 | display: flex; 69 | align-items: center; 70 | line-height: 3rem; 71 | width: 100%; 72 | } 73 | 74 | .nav-item ::deep a.active { 75 | background-color: rgba(255,255,255,0.37); 76 | color: white; 77 | } 78 | 79 | .nav-item ::deep .nav-link:hover { 80 | background-color: rgba(255,255,255,0.1); 81 | color: white; 82 | } 83 | 84 | .nav-scrollable { 85 | display: none; 86 | } 87 | 88 | .navbar-toggler:checked ~ .nav-scrollable { 89 | display: block; 90 | } 91 | 92 | @media (min-width: 641px) { 93 | .navbar-toggler { 94 | display: none; 95 | } 96 | 97 | .nav-scrollable { 98 | /* Never collapse the sidebar for wide screens */ 99 | display: block; 100 | 101 | /* Allow sidebar to scroll for tall menus */ 102 | height: calc(100vh - 3.5rem); 103 | overflow-y: auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/Shared/RedirectToLogin.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | @code { 4 | protected override void OnInitialized() 5 | { 6 | NavigationManager.NavigateTo($"Login?returnUrl={Uri.EscapeDataString(NavigationManager.Uri)}", forceLoad: true); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorCleanArchitecture.WebUI.Server 10 | @using BlazorCleanArchitecture.WebUI.Server.Shared 11 | @using BlazorCleanArchitecture.Domain.Articles 12 | @using BlazorCleanArchitecture.Application.Articles 13 | @using MediatR 14 | @using BlazorCleanArchitecture.Application.Articles.CreateArticle 15 | @using BlazorCleanArchitecture.Application.Articles.GetArticles 16 | @using BlazorCleanArchitecture.Application.Articles.GetArticleById 17 | @using BlazorCleanArchitecture.Application.Articles.UpdateArticle 18 | @using BlazorCleanArchitecture.Application.Articles.DeleteArticle 19 | @using Mapster 20 | @using Microsoft.AspNetCore.Components.Authorization 21 | @using Microsoft.AspNetCore.Authorization 22 | @using BlazorCleanArchitecture.WebUI.Server.Features.Users.Component 23 | @using BlazorCleanArchitecture.Application.Users 24 | @using Microsoft.AspNetCore.Components.QuickGrid 25 | @using BlazorCleanArchitecture.Application.Users.GetUsers 26 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=localhost;Database=BlazingBlog;Integrated Security=True;Encrypt=True; TrustServerCertificate=True;" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft.AspNetCore": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*" 12 | } 13 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | input { 53 | color-scheme: dark; 54 | } 55 | -------------------------------------------------------------------------------- /BlazingBlog.WebUI.Server/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RobHutton/Blazor-Clean-Architecture/cebda85bda1db266ad9a70958270b40c533a99ab/BlazingBlog.WebUI.Server/wwwroot/favicon.png -------------------------------------------------------------------------------- /BlazorCleanArchitecture.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.11.35222.181 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{35E2C40F-DF84-4FEE-AA87-AA171A5149EA}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorCleanArchitecture.WebUI.Server", "BlazingBlog.WebUI.Server\BlazorCleanArchitecture.WebUI.Server.csproj", "{A2AB928F-0E58-427D-8134-2396CA4DA252}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorCleanArchitecture.Domain", "BlazingBlog.Domain\BlazorCleanArchitecture.Domain.csproj", "{1A6A9D83-EC6D-4A3F-AAA1-13C79729C756}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorCleanArchitecture.Application", "BlazingBlog.Application\BlazorCleanArchitecture.Application.csproj", "{39E58F53-50FA-42CF-A5D3-8C2184DBEBA8}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorCleanArchitecture.Infrastructure", "BlazingBlog.Infrastructure\BlazorCleanArchitecture.Infrastructure.csproj", "{C35D80E2-B8BB-4316-B34C-4CC6FEC0A74F}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorCleanArchitecture.WebUI.Client", "BlazingBlog.WebUI.Client\BlazorCleanArchitecture.WebUI.Client.csproj", "{7BCA1086-90AE-45DD-9B62-95243D08CD28}" 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 | {A2AB928F-0E58-427D-8134-2396CA4DA252}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {A2AB928F-0E58-427D-8134-2396CA4DA252}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {A2AB928F-0E58-427D-8134-2396CA4DA252}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {A2AB928F-0E58-427D-8134-2396CA4DA252}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {1A6A9D83-EC6D-4A3F-AAA1-13C79729C756}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {1A6A9D83-EC6D-4A3F-AAA1-13C79729C756}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {1A6A9D83-EC6D-4A3F-AAA1-13C79729C756}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {1A6A9D83-EC6D-4A3F-AAA1-13C79729C756}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {39E58F53-50FA-42CF-A5D3-8C2184DBEBA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {39E58F53-50FA-42CF-A5D3-8C2184DBEBA8}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {39E58F53-50FA-42CF-A5D3-8C2184DBEBA8}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {39E58F53-50FA-42CF-A5D3-8C2184DBEBA8}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {C35D80E2-B8BB-4316-B34C-4CC6FEC0A74F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {C35D80E2-B8BB-4316-B34C-4CC6FEC0A74F}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {C35D80E2-B8BB-4316-B34C-4CC6FEC0A74F}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {C35D80E2-B8BB-4316-B34C-4CC6FEC0A74F}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {7BCA1086-90AE-45DD-9B62-95243D08CD28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {7BCA1086-90AE-45DD-9B62-95243D08CD28}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {7BCA1086-90AE-45DD-9B62-95243D08CD28}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {7BCA1086-90AE-45DD-9B62-95243D08CD28}.Release|Any CPU.Build.0 = Release|Any CPU 44 | EndGlobalSection 45 | GlobalSection(SolutionProperties) = preSolution 46 | HideSolutionNode = FALSE 47 | EndGlobalSection 48 | GlobalSection(NestedProjects) = preSolution 49 | {A2AB928F-0E58-427D-8134-2396CA4DA252} = {35E2C40F-DF84-4FEE-AA87-AA171A5149EA} 50 | {1A6A9D83-EC6D-4A3F-AAA1-13C79729C756} = {35E2C40F-DF84-4FEE-AA87-AA171A5149EA} 51 | {39E58F53-50FA-42CF-A5D3-8C2184DBEBA8} = {35E2C40F-DF84-4FEE-AA87-AA171A5149EA} 52 | {C35D80E2-B8BB-4316-B34C-4CC6FEC0A74F} = {35E2C40F-DF84-4FEE-AA87-AA171A5149EA} 53 | {7BCA1086-90AE-45DD-9B62-95243D08CD28} = {35E2C40F-DF84-4FEE-AA87-AA171A5149EA} 54 | EndGlobalSection 55 | GlobalSection(ExtensibilityGlobals) = postSolution 56 | SolutionGuid = {20B5EE19-0224-4E3F-92D5-BA8F0079AF80} 57 | EndGlobalSection 58 | EndGlobal 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor Clean Architecture + CQRS with MediatR 2 | 3 | In the evolving landscape of web development, Blazor stands out as a revolutionary framework that empowers developers to build rich, interactive web applications with the elegance of C# and the power of .NET 8. By harnessing the capabilities of Single-Page Applications (SPAs), Blazor provides a robust environment that caters to both client-side and server-side execution, ensuring fast, real-time updates and seamless user experiences. This article delves into the intricacies of Blazor, exploring its architecture, including Clean Architecture, the CQRS pattern with MediatR, and the innovative Vertical Slice Architecture that streamlines the development process. 4 | 5 | Ask me why I love Blazor. 6 | I love Blazor development because it is a complete Single-Page Application (SPA) UI framework for building rich, interactive web applications using .NET 8 framework and C# 12. 7 | 8 | Blazor offers flexibility with options like WebAssembly for full client-side execution and Blazor Server for fast, real-time updates. With Server-Side Rendering (SSR), it improves performance and SEO, and Blazor Auto dynamically selects the best rendering mode. It enables C# coding everywhere, is blazing fast, and eliminates the need for JavaScript for most tasks. 9 | 10 | Blazor Hosting Models 11 | Blazor Server Side Rendered (SSR) Mode generates and sends static HTML from the server to the client, and the rendering happens entirely on the server. After the initial render, the client can interact with the server for more updates. 12 | 13 | Rendering: Server-side. 14 | Interactivity: Typically involves round-trips to the server for user interactions (like clicking buttons), unless JavaScript is used. 15 | Performance: Faster initial load because only HTML is sent. However, user interactions may feel slower because every action requires a server round-trip. 16 | Use Case: Suitable for pages where SEO and quick first-page load are important. 17 | Stream Rendering allows parts of a Blazor application to be progressively rendered and sent to the client as they are completed, rather than waiting for the entire page to be processed. 18 | 19 | Rendering: Server-side, but streamed in chunks. 20 | Interactivity: User can start interacting with visible parts of the UI while the rest of the page is still being rendered. 21 | Performance: Improves perceived load times by sending the initial parts of the page early. User interactivity is enabled as sections become available. 22 | Use Case: Ideal for pages with heavy content or complex UI where rendering different parts at different times provides a smoother experience. 23 | Blazor Server Components run on the server but update the UI in the browser via a real-time SignalR connection. These components render HTML on the server and send a “render tree” (UI diff) to the client for updates. 24 | 25 | Rendering: Server-side, with real-time updates via SignalR. 26 | Interactivity: Highly interactive, as the user interacts with the UI, the changes are processed on the server, and only the UI diffs are sent back to the client. 27 | Performance: Faster interactivity compared to SSR, but requires a constant server connection and low latency for the best performance. 28 | Use Case: Ideal for internal business applications where maintaining a connection and real-time updates are crucial, and you want to avoid downloading the full app to the client. 29 | Blazor Web Assembly Components (WASM) run entirely in the client’s browser using WebAssembly. The components are downloaded as part of the initial application, and the rendering logic happens client-side. 30 | 31 | Rendering: Client-side. 32 | Interactivity: All interactions are handled client-side, which results in faster interactions because there’s no need to contact the server for each action. 33 | Performance: Slower initial load (due to downloading the application), but very fast once the app is fully loaded in the browser. Can run offline. 34 | Use Case: Great for applications that require rich client-side interactivity, offline capabilities, or where a persistent server connection isn’t feasible. 35 | Auto Mode (Server to WebAssembly Transition) Blazor can start rendering components on the server (like Blazor Server) and then transition to WebAssembly on the client when the WebAssembly runtime becomes available. 36 | 37 | Rendering: Begins with server-side rendering (SSR), then switches to client-side WebAssembly rendering as the WASM runtime is downloaded. 38 | Interactivity: Initially, interactions are processed server-side, but after transitioning to WebAssembly, all interactivity happens on the client. 39 | Performance: Faster initial load because the application starts as an SSR app, and then transitions to a client-side WebAssembly app to provide a more interactive experience without server round-trips. 40 | Use Case: Suitable for apps that want the best of both worlds — fast initial loads with SSR and fast interactivity with WASM after the transition. This mode is beneficial when you want SEO support and fast interactions after the initial page load. 41 | What is Clean Architecture? 42 | Presentation/UI => Infrastructure => Application => Domain 43 | 44 | Clean Architecture organizes a project in such a way that ensures separation of concerns by decoupling different aspects of the application. Clean Architecture typically includes the following Layers, which are included in the Solution as separate projects: 45 | 46 | Domain Layer (Entities) 47 | Inner-most layer represents the core business objects of the application. These entities capture business concepts and are independent of other layers. 48 | References: 49 | 50 | Project References: None 51 | Example: In a Blazor app managing orders, an Order class would be an entity. 52 | Application Layer (Use Cases) 53 | Contains the business rules and defines what operations can be performed using the entities. 54 | 55 | Project References: Domain Layer 56 | Example: A use case for placing an order might include order validation, updating inventory, and sending notifications. 57 | Infrastructure Layer 58 | Implements the interfaces to interact with external systems like databases, APIs, or file systems. 59 | 60 | Project References: Application Layer 61 | Example: An implementation of IOrderRepository using Entity Framework or Dapper to access a SQL database. 62 | Presentation/UI Layer 63 | Handles user interaction and displays data using Razor Components in Blazor. This layer communicates with use cases to retrieve or modify data. 64 | 65 | Project References: Infrastructure Layer 66 | Example: Blazor components such as OrderList.razor or OrderDetails.razor are part of this layer, fetching and displaying order data. 67 | CQRS + MediatR Pattern 68 | CQRS is a software pattern that separates reading (query) and writing (command) logic, which further promotes separation of concerns and decoupling. In addition, the CQRS pattern allows us to optimize database reads and writes separately for scalability, caching and data consistency. 69 | 70 | The Mediator Pattern is a behavioral design pattern that helps in reducinthe coupling between objects by ensuring that objects communicate with each other through a mediator instead of directly. 71 | 72 | 73 | Command: The component sends a Command (e.g., CreateOrderCommand) to the Mediator. The Mediator routes this command to the correct Handler in the Application Layer. The Handler performs the business logic, interacting with the Domain Layer or Infrastructure Layer to persist data. 74 | Query: If a query is made (e.g., fetching orders), a Query (e.g., GetOrdersQuery) is sent to the Mediator, which routes it to the corresponding QueryHandler that fetches data and returns it. 75 | Example: This example creates an ICommand and ICommandHandler using MediatR and wrapping the results with OK or Fail. 76 | 77 | public interface ICommand : IRequest 78 | { 79 | } 80 | public interface ICommand : IRequest> 81 | { 82 | } 83 | 84 | // CQRS Pattern 85 | // Command Handler for commands that do not return any specific result other than success or failure 86 | public interface ICommandHandler : IRequestHandler 87 | where TCommand : ICommand 88 | { 89 | } 90 | // CQRS Pattern 91 | // Command Handler for commands that do return a specific response type 92 | public interface ICommandHandler : IRequestHandler> 93 | where TCommand : ICommand 94 | { 95 | } 96 | Example: Executing a Command using ICommandHandler. This example uses Microsoft.Identity for authorization and Mapster Auto-Mapper to convert result to Data Transformation Object, which is a simple and lightweight object used to communicate between Layers. 97 | 98 | // Article Data Transformation Object 99 | // Used to Communicate with the Client 100 | public record struct ArticleDto( 101 | int Id, 102 | string Title, 103 | string? Content, 104 | DateTime DatePublished, 105 | bool IsPublished, 106 | string UserName, 107 | string UserId, 108 | bool CanEdit 109 | ) 110 | { } 111 | 112 | // Command Parameters 113 | public class CreateArticleCommand : ICommand 114 | { 115 | public required string Title { get; set; } 116 | public required string Content { get; set; } 117 | public DateTime DatePublished { get; set; } = DateTime.Now; 118 | public bool IsPublished { get; set; } = false; 119 | } 120 | 121 | public class CreateArticleCommandHandler : ICommandHandler 122 | { 123 | private readonly IArticleRepository _articleRepository; 124 | private readonly IUserService _userService; 125 | public CreateArticleCommandHandler(IArticleRepository articleRepository, IUserService userService) 126 | { 127 | _articleRepository = articleRepository; 128 | _userService = userService; 129 | } 130 | public async Task> Handle(CreateArticleCommand request, CancellationToken cancellationToken) 131 | { 132 | try 133 | { 134 | var newArticle = request.Adapt
(); 135 | newArticle.UserId = await _userService.GetCurrentUserIdAsync(); 136 | if (!await _userService.CurrentUserCanCreateArticleAsync()) 137 | { 138 | return FailingResult("You are not authorized to create an article."); 139 | } 140 | var article = await _articleRepository.CreateArticleAsync(newArticle); 141 | return article.Adapt(); 142 | } 143 | catch (UserNotAuthorizedException) 144 | { 145 | return FailingResult("An error occurred creating the article."); 146 | } 147 | 148 | } 149 | private Result FailingResult(string msg) 150 | { 151 | return Result.Fail(msg ?? "Failed to create article."); 152 | } 153 | } 154 | Vertical Slice Architecture 155 | With Blazor development, I typically prefer a Vertical Slice Architecture, which organizes source code by feature rather than by technology. This approach promotes better separation of concerns, enhances maintainability, and allows teams to work more autonomously. 156 | 157 | 158 | Feature-Focused: Each vertical slice contains everything needed to implement a specific feature or functionality, including UI, business logic, and data access code. 159 | Independently Deployable: Since each slice is a self-contained unit, it can be developed, tested, and deployed independently, making continuous integration and deployment easier. 160 | Enhanced Collaboration: Teams can work on different slices concurrently, reducing dependencies and enabling faster delivery of features. 161 | Improved Maintainability: By grouping related code together, it becomes easier to understand and maintain the functionality associated with a specific feature. 162 | Easier Testing: Testing can be more straightforward since each slice can be tested in isolation, ensuring that all components related to a feature are working as intended. 163 | Example: In the Application Layer, there is a folder called Articles that handles all of the article-related functionality. 164 | 165 | 166 | In conclusion, Blazor represents a significant leap forward in web application development, marrying the benefits of modern frameworks with the reliability of C#. Its flexible architecture, combined with patterns like Clean Architecture and CQRS, enhances maintainability and scalability, allowing developers to create high-quality applications with ease. As Blazor continues to evolve, its potential to transform the development landscape is undeniable, making it an essential tool for any developer looking to build the next generation of web applications. Embracing Blazor not only facilitates faster development cycles but also fosters a deeper connection with the underlying business logic, enabling the creation of truly impactful digital experiences. 167 | --------------------------------------------------------------------------------