├── .gitignore ├── Blazorcrud.Client ├── App.razor ├── Blazorcrud.Client.csproj ├── Pages │ ├── Index.razor │ ├── Person │ │ ├── Add.razor │ │ ├── Form.razor │ │ ├── List.razor │ │ ├── Update.razor │ │ └── View.razor │ ├── Upload │ │ ├── Add.razor │ │ ├── List.razor │ │ └── View.razor │ └── Users │ │ ├── Add.razor │ │ ├── Form.razor │ │ ├── List.razor │ │ ├── Login.razor │ │ ├── Logout.razor │ │ ├── Update.razor │ │ └── View.razor ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── IPersonService.cs │ ├── IUploadService.cs │ ├── IUserService.cs │ ├── PersonService.cs │ ├── UploadService.cs │ └── UserService.cs ├── Shared │ ├── Alert.razor │ ├── AlertModel.cs │ ├── AlertService.cs │ ├── AppRouteView.cs │ ├── FluentValidator.cs │ ├── HttpService.cs │ ├── LocalStorageService.cs │ ├── Login.cs │ ├── MainLayout.razor │ ├── PageHistoryState.cs │ ├── Pager.razor │ ├── Pager.razor.css │ ├── QueryExtensionMethods.cs │ └── StringConverter.cs ├── _Imports.razor └── wwwroot │ ├── css │ ├── app.css │ ├── bootstrap │ │ ├── bootstrap.min.css │ │ └── bootstrap.min.css.map │ └── open-iconic │ │ ├── FONT-LICENSE │ │ ├── ICON-LICENSE │ │ ├── README.md │ │ └── font │ │ ├── css │ │ └── open-iconic-bootstrap.min.css │ │ └── fonts │ │ ├── open-iconic.eot │ │ ├── open-iconic.otf │ │ ├── open-iconic.svg │ │ ├── open-iconic.ttf │ │ └── open-iconic.woff │ ├── favicon.ico │ ├── index.html │ └── site.js ├── Blazorcrud.Server ├── Authorization │ ├── AllowAnonymouseAttribute.cs │ ├── AuthenticateRequest.cs │ ├── AuthenticateResponse.cs │ ├── AuthorizeAttribute.cs │ ├── JwtMiddleware.cs │ └── JwtUtils.cs ├── Blazorcrud.Server.csproj ├── Controllers │ ├── PersonController.cs │ ├── UploadController.cs │ └── UserController.cs ├── Helpers │ ├── AppException.cs │ └── ErrorHandlerMiddleware.cs ├── Models │ ├── AppDbContext.cs │ ├── DataGenerator.cs │ ├── IPersonRepository.cs │ ├── IUploadRepository.cs │ ├── IUserRepository.cs │ ├── PersonRepository.cs │ ├── UploadRepository.cs │ └── UserRepository.cs ├── Pages │ ├── Error.cshtml │ └── Error.cshtml.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── Services │ ├── ServiceCollectionQuartzConfigurationExtensions.cs │ └── UploadProcessorJob.cs ├── appsettings.Development.json ├── appsettings.json └── test.http ├── Blazorcrud.Shared ├── Blazorcrud.Shared.csproj ├── Data │ ├── PagedResultBase.cs │ ├── PagedResultExtension.cs │ └── PagedResultT.cs └── Models │ ├── Address.cs │ ├── AddressValidator.cs │ ├── Gender.cs │ ├── Person.cs │ ├── PersonValidator.cs │ ├── Upload.cs │ ├── UploadValidator.cs │ ├── User.cs │ └── UserValidator.cs ├── Blazorcrud.sln ├── Create-Env.bash ├── Delete-env.bash ├── Dockerfile ├── LICENSE ├── README.md └── azure-pipelines.yml /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure configuration files 5 | *.azureauth 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /Blazorcrud.Client/App.razor: -------------------------------------------------------------------------------- 1 | @inject NavigationManager NavigationManager 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | @{ 10 | // redirect to homepage if not found 11 | NavigationManager.NavigateTo("/"); 12 | } 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Blazorcrud.Client/Blazorcrud.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | disable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 |
4 |

Blazor CRUD

5 |

A complete project framework that has everything you need to build your Blazor WASM enterprise application.

6 |
7 | 14 |
-------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Person/Add.razor: -------------------------------------------------------------------------------- 1 | @page "/person/createperson" 2 | @attribute [Authorize] 3 | @inject IAlertService AlertService 4 | @inject IPersonService PersonService 5 | @inject NavigationManager navManager 6 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 7 | 8 |

Create Person

9 |
10 | 11 |
13 | 14 | @code { 15 | bool loading = false; 16 | 17 | Person person = new Person 18 | { 19 | FirstName = "", 20 | LastName = "", 21 | Gender = Gender.Other, 22 | PhoneNumber = "", 23 | Addresses = new List
24 | { 25 | new Address {Street="", City="", State="", ZipCode=""} 26 | } 27 | }; 28 | async Task CreatePerson() 29 | { 30 | loading = true; 31 | try 32 | { 33 | await PersonService.AddPerson(person); 34 | AlertService.Success("Person added successfully", keepAfterRouteChange: true); 35 | if (PageHistoryState.CanGoBack()){ 36 | navManager.NavigateTo(PageHistoryState.GetGoBackPage()); 37 | } 38 | else{ 39 | navManager.NavigateTo("/person/1"); 40 | } 41 | } 42 | catch (Exception ex) 43 | { 44 | AlertService.Error(ex.Message); 45 | loading = false; 46 | StateHasChanged(); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Person/Form.razor: -------------------------------------------------------------------------------- 1 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 |
32 |
33 |
34 |
35 | 36 |
37 | 38 | 39 |
40 |
41 |
42 | 43 |
44 |

Addresses

45 |
46 |
47 | 48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 |
58 |
59 | 60 | 61 | @foreach (var address in person.Addresses) 62 | { 63 |
64 |
65 | 66 | 67 |
68 |
69 | 70 | 71 |
72 |
73 | 74 | 75 |
76 |
77 | 78 | 79 |
80 |
81 | X 82 |
83 |
84 | } 85 |
86 | Add Address 87 |
88 |
89 | 90 |
91 | 98 | @if (PageHistoryState.CanGoBack()){ 99 | Cancel 100 | } 101 | else{ 102 | Back 103 | } 104 |
105 | 106 |
107 | 108 | @code { 109 | [Parameter] 110 | public Person person { get; set; } 111 | [Parameter] 112 | public string ButtonText { get; set; } = "Save"; 113 | [Parameter] 114 | public bool loading {get; set;} = false; 115 | [Parameter] 116 | public EventCallback OnValidSubmit { get; set; } 117 | 118 | public void OnAddressDelete(Person person, Address address) 119 | { 120 | person.Addresses.Remove(address); 121 | } 122 | 123 | public void OnAddressAdd(Person person) 124 | { 125 | person.Addresses.Add(new Address { Street = "", City = "", State = "", ZipCode="" }); 126 | } 127 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Person/List.razor: -------------------------------------------------------------------------------- 1 | @page "/person/{Page}" 2 | @inject Services.IPersonService PersonService 3 | @inject Services.IUserService UserService 4 | @inject Microsoft.AspNetCore.Components.NavigationManager UriHelper 5 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 6 | 7 |

People

8 |
9 | 10 |
11 |
12 | @if(LoggedIn) 13 | { 14 | Add Person 15 | } 16 |
17 |
18 | 19 |
20 | 23 |
24 |
25 | 28 |
29 |
30 |
31 | 32 | @if (people == null) 33 | { 34 |

Loading...

35 | } 36 | else 37 | { 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | @foreach (var person in people.Results) 50 | { 51 | 52 | 53 | 54 | 55 | 56 | 76 | 77 | } 78 | 79 |
First NameLast NameGenderPhone Number
@person.FirstName@person.LastName@person.Gender@person.PhoneNumber 57 | 58 | @if (LoggedIn) 59 | { 60 | 61 | 62 | @if (person.IsDeleting) 63 | { 64 | 67 | } 68 | else 69 | { 70 | 73 | } 74 | } 75 |
80 | 81 | } 82 | 83 | @code { 84 | [Parameter] 85 | public string Page { get; set;} = "1"; 86 | [Parameter] 87 | public string SearchTerm { get; set; } = string.Empty; 88 | protected PagedResult people; 89 | 90 | public bool LoggedIn 91 | { 92 | get {return UserService.User != null;} 93 | } 94 | 95 | protected override void OnInitialized() 96 | { 97 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 98 | base.OnInitialized(); 99 | } 100 | 101 | protected override async Task OnParametersSetAsync() 102 | { 103 | people = await PersonService.GetPeople(null, Page); 104 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 105 | } 106 | 107 | protected async Task SearchBoxKeyPress(KeyboardEventArgs ev) 108 | { 109 | if (ev.Key == "Enter") 110 | { 111 | await SearchClick(); 112 | } 113 | } 114 | 115 | protected async Task SearchClick() 116 | { 117 | if (string.IsNullOrEmpty(SearchTerm)) 118 | { 119 | people = await PersonService.GetPeople(null, Page); 120 | return; 121 | } 122 | people = await PersonService.GetPeople(SearchTerm, Page); 123 | StateHasChanged(); 124 | } 125 | 126 | protected async Task ClearSearch() 127 | { 128 | SearchTerm = string.Empty; 129 | people = await PersonService.GetPeople(SearchTerm, Page); 130 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 131 | StateHasChanged(); 132 | } 133 | 134 | protected void PagerPageChanged(int page) 135 | { 136 | UriHelper.NavigateTo("/person/" + page); 137 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 138 | } 139 | 140 | private async void DeletePerson(Person _person) 141 | { 142 | var person = _person; 143 | person.IsDeleting = true; 144 | await PersonService.DeletePerson(person.PersonId); 145 | people = await PersonService.GetPeople(null, Page); 146 | StateHasChanged(); 147 | } 148 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Person/Update.razor: -------------------------------------------------------------------------------- 1 | @page "/person/updateperson/{id}" 2 | @attribute [Authorize] 3 | @inject IAlertService AlertService 4 | @inject IPersonService PersonService 5 | @inject NavigationManager navManager 6 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 7 | 8 |

Edit Person

9 |
10 | 11 | 13 | 14 | @code { 15 | bool loading = false; 16 | 17 | [Parameter] 18 | public string Id { get; set; } 19 | 20 | Person person = new Person 21 | { 22 | FirstName = "", 23 | LastName = "", 24 | Gender = Gender.Other, 25 | PhoneNumber = "", 26 | Addresses = new List
27 | { 28 | new Address {Street="", City="", State="", ZipCode=""} 29 | } 30 | }; 31 | 32 | protected async override Task OnParametersSetAsync() 33 | { 34 | person = await PersonService.GetPerson(int.Parse(Id)); 35 | } 36 | 37 | async Task UpdatePerson() 38 | { 39 | loading = true; 40 | try 41 | { 42 | await PersonService.UpdatePerson(person); 43 | AlertService.Success("Person updated successfully", keepAfterRouteChange: true); 44 | if (PageHistoryState.CanGoBack()){ 45 | navManager.NavigateTo(PageHistoryState.GetGoBackPage()); 46 | } 47 | else{ 48 | navManager.NavigateTo("/person/1"); 49 | } 50 | } 51 | catch (Exception ex) 52 | { 53 | AlertService.Error(ex.Message); 54 | loading = false; 55 | StateHasChanged(); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Person/View.razor: -------------------------------------------------------------------------------- 1 | @page "/person/viewperson/{id}" 2 | @inject IPersonService PersonService 3 | @inject NavigationManager navManager 4 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 5 | 6 |

View Person

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Id@person.PersonId
First Name@person.FirstName
Last Name@person.LastName
Gender@person.Gender
Phone Number@person.PhoneNumber
33 | 34 |
35 |

Addresses

36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | @foreach (var address in person.Addresses) 50 | { 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | } 59 | 60 |
IdStreetCityStateZipCode
@address.AddressId@address.Street@address.City@address.State@address.ZipCode
61 |
62 | @if (PageHistoryState.CanGoBack()){ 63 | Back 64 | } 65 | else{ 66 | Back 67 | } 68 |
69 | 70 | @code { 71 | [Parameter] 72 | public string Id { get; set; } 73 | 74 | Person person = new Person 75 | { 76 | FirstName = "", 77 | LastName = "", 78 | Gender = Gender.Other, 79 | PhoneNumber = "", 80 | Addresses = new List
81 | { 82 | new Address {Street="", City="", State="", ZipCode=""} 83 | } 84 | }; 85 | 86 | protected async override Task OnParametersSetAsync() 87 | { 88 | person = await PersonService.GetPerson(int.Parse(Id)); 89 | } 90 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Upload/Add.razor: -------------------------------------------------------------------------------- 1 | @page "/upload/createupload" 2 | @attribute [Authorize] 3 | @inject IAlertService AlertService 4 | @inject IUploadService UploadService 5 | @inject NavigationManager navManager 6 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 7 | 8 |

Create Upload

9 |
10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 | 18 |
19 |
20 |
21 |
22 | 23 |
24 | 25 | 26 |
27 |
28 |
29 | 30 |
31 | 38 | @if (PageHistoryState.CanGoBack()){ 39 | Back 40 | } 41 | else{ 42 | Back 43 | } 44 |
45 | 46 |
47 | 48 | @code { 49 | [Parameter] 50 | public EventCallback OnValidSubmit { get; set; } 51 | bool loading = false; 52 | string status = "File pending upload..."; 53 | 54 | Upload upload = new Upload 55 | { 56 | FileName = "", 57 | UploadTimestamp = DateTime.Now, 58 | ProcessedTimestamp = null 59 | }; 60 | 61 | async Task HandleSelection(InputFileChangeEventArgs e) 62 | { 63 | var file = e.File; 64 | if (file != null) 65 | { 66 | // Load into memory 67 | var buffer = new byte[file.Size]; 68 | await file.OpenReadStream().ReadAsync(buffer); 69 | status = $"Finished loading {file.Size} bytes from {file.Name}"; 70 | upload.FileContent = Convert.ToBase64String(buffer); 71 | } 72 | } 73 | 74 | async Task CreateUpload() 75 | { 76 | loading = true; 77 | try 78 | { 79 | await UploadService.AddUpload(upload); 80 | AlertService.Success("Upload added successfully", keepAfterRouteChange: true); 81 | if (PageHistoryState.CanGoBack()){ 82 | navManager.NavigateTo(PageHistoryState.GetGoBackPage()); 83 | } 84 | else{ 85 | navManager.NavigateTo("/upload/1"); 86 | } 87 | } 88 | catch (Exception ex) 89 | { 90 | AlertService.Error(ex.Message); 91 | loading = false; 92 | StateHasChanged(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Upload/List.razor: -------------------------------------------------------------------------------- 1 | @page "/upload/{Page}" 2 | @inject Services.IUploadService UploadService 3 | @inject Services.IUserService UserService 4 | @inject Microsoft.AspNetCore.Components.NavigationManager UriHelper 5 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 6 | 7 |

Uploads

8 |
9 | 10 |
11 |
12 | @if(LoggedIn) 13 | { 14 | Add Upload 15 | } 16 |
17 |
18 | 19 |
20 | 23 |
24 |
25 | 28 |
29 |
30 |
31 | 32 | @if (uploads == null) 33 | { 34 |

Loading...

35 | } 36 | else 37 | { 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | @foreach (var upload in uploads.Results) 49 | { 50 | 51 | 52 | 53 | 54 | 57 | 58 | } 59 | 60 |
File NameUploadedProcessed
@upload.FileName@upload.UploadTimestamp@upload.ProcessedTimestamp 55 | 56 |
61 | 62 | } 63 | 64 | @code { 65 | [Parameter] 66 | public string Page { get; set;} = "1"; 67 | [Parameter] 68 | public string SearchTerm { get; set; } = string.Empty; 69 | protected PagedResult uploads; 70 | 71 | public bool LoggedIn 72 | { 73 | get {return UserService.User != null;} 74 | } 75 | 76 | protected override async Task OnParametersSetAsync() 77 | { 78 | uploads = await UploadService.GetUploads(null, Page); 79 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 80 | } 81 | 82 | protected async Task SearchBoxKeyPress(KeyboardEventArgs ev) 83 | { 84 | if (ev.Key == "Enter") 85 | { 86 | await SearchClick(); 87 | } 88 | } 89 | 90 | protected async Task SearchClick() 91 | { 92 | if (string.IsNullOrEmpty(SearchTerm)) 93 | { 94 | uploads = await UploadService.GetUploads(null, Page); 95 | return; 96 | } 97 | uploads = await UploadService.GetUploads(SearchTerm, Page); 98 | StateHasChanged(); 99 | } 100 | 101 | protected async Task ClearSearch() 102 | { 103 | SearchTerm = string.Empty; 104 | uploads = await UploadService.GetUploads(SearchTerm, Page); 105 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 106 | StateHasChanged(); 107 | } 108 | protected void PagerPageChanged(int page) 109 | { 110 | UriHelper.NavigateTo("/upload/" + page); 111 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 112 | } 113 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Upload/View.razor: -------------------------------------------------------------------------------- 1 | @page "/upload/viewupload/{id}" 2 | @inject IUploadService UploadService 3 | @inject NavigationManager navManager 4 | @inject IJSRuntime JSRuntime 5 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 6 | 7 |

View Upload

8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 |
Id@upload.Id
Uploaded At@upload.UploadTimestamp
Processed At@upload.ProcessedTimestamp
File 27 | 28 |
32 | 33 |
34 | @if (PageHistoryState.CanGoBack()){ 35 | Back 36 | } 37 | else{ 38 | Back 39 | } 40 |
41 | 42 | @code { 43 | [Parameter] 44 | public string Id { get; set; } 45 | 46 | Upload upload = new Upload(); 47 | 48 | protected async override Task OnParametersSetAsync() 49 | { 50 | upload = await UploadService.GetUpload(int.Parse(Id)); 51 | } 52 | 53 | void DownloadFile() 54 | { 55 | JSRuntime.InvokeAsync( 56 | "saveAsFile", 57 | upload.FileName, 58 | upload.FileContent); 59 | } 60 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/Add.razor: -------------------------------------------------------------------------------- 1 | @page "/user/createuser" 2 | @attribute [Authorize] 3 | @inject IAlertService AlertService 4 | @inject IUserService UserService 5 | @inject NavigationManager navManager 6 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 7 | 8 |

Create User

9 |
10 | 11 | 13 | 14 | @code { 15 | bool loading = false; 16 | 17 | User user = new User(); 18 | 19 | async Task CreateUser() 20 | { 21 | loading = true; 22 | try 23 | { 24 | await UserService.AddUser(user); 25 | AlertService.Success("User added successfully", keepAfterRouteChange: true); 26 | if (PageHistoryState.CanGoBack()){ 27 | navManager.NavigateTo(PageHistoryState.GetGoBackPage()); 28 | } 29 | else{ 30 | navManager.NavigateTo("/user/1"); 31 | } 32 | } 33 | catch (Exception ex) 34 | { 35 | AlertService.Error(ex.Message); 36 | loading = false; 37 | StateHasChanged(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/Form.razor: -------------------------------------------------------------------------------- 1 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 2 | 3 | 4 | 5 |
6 | 7 |
8 | 9 | 10 |
11 |
12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 | 21 |
22 | 23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 | 31 |
32 |
33 | 34 |
35 | 42 | @if (PageHistoryState.CanGoBack()){ 43 | Cancel 44 | } 45 | else{ 46 | Back 47 | } 48 |
49 | 50 |
51 | 52 | @code { 53 | [Parameter] 54 | public User user { get; set; } 55 | [Parameter] 56 | public string ButtonText { get; set; } = "Save"; 57 | [Parameter] 58 | public bool loading {get; set;} = false; 59 | [Parameter] 60 | public EventCallback OnValidSubmit { get; set; } 61 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/List.razor: -------------------------------------------------------------------------------- 1 | @page "/user/{Page}" 2 | @inject Services.IUserService UserService 3 | @inject Microsoft.AspNetCore.Components.NavigationManager UriHelper 4 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 5 | 6 |

Users

7 |
8 | 9 |
10 | 11 |
12 | @if(LoggedIn) 13 | { 14 | Add User 15 | } 16 |
17 |
18 | 19 |
20 | 23 |
24 |
25 | 28 |
29 |
30 |
31 | 32 | @if (users == null) 33 | { 34 |

Loading...

35 | } 36 | else 37 | { 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | @foreach (var user in users.Results) 49 | { 50 | 51 | 52 | 53 | 54 | 74 | 75 | } 76 | 77 |
First NameLast NameUsername
@user.FirstName@user.LastName@user.Username 55 | 56 | @if (LoggedIn && user.Username != "admin") 57 | { 58 | 59 | 60 | @if (user.IsDeleting) 61 | { 62 | 65 | } 66 | else 67 | { 68 | 71 | } 72 | } 73 |
78 | 79 | } 80 | 81 | @code { 82 | [Parameter] 83 | public string Page { get; set;} = "1"; 84 | [Parameter] 85 | public string SearchTerm { get; set; } = string.Empty; 86 | protected PagedResult users; 87 | 88 | public bool LoggedIn 89 | { 90 | get {return UserService.User != null;} 91 | } 92 | 93 | protected override void OnInitialized() 94 | { 95 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 96 | base.OnInitialized(); 97 | } 98 | 99 | protected override async Task OnParametersSetAsync() 100 | { 101 | users = await UserService.GetUsers(null, Page); 102 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 103 | } 104 | 105 | protected async Task SearchBoxKeyPress(KeyboardEventArgs ev) 106 | { 107 | if (ev.Key == "Enter") 108 | { 109 | await SearchClick(); 110 | } 111 | } 112 | 113 | protected async Task SearchClick() 114 | { 115 | if (string.IsNullOrEmpty(SearchTerm)) 116 | { 117 | users = await UserService.GetUsers(null, Page); 118 | return; 119 | } 120 | users = await UserService.GetUsers(SearchTerm, Page); 121 | StateHasChanged(); 122 | } 123 | 124 | protected async Task ClearSearch() 125 | { 126 | SearchTerm = string.Empty; 127 | users = await UserService.GetUsers(SearchTerm, Page); 128 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 129 | StateHasChanged(); 130 | } 131 | 132 | protected void PagerPageChanged(int page) 133 | { 134 | UriHelper.NavigateTo("/user/" + page); 135 | PageHistoryState.AddPageToHistory(UriHelper.Uri); 136 | } 137 | 138 | private async void DeleteUser(User _user) 139 | { 140 | var user = _user; 141 | user.IsDeleting = true; 142 | await UserService.DeleteUser(user.Id); 143 | users = await UserService.GetUsers(null, Page); 144 | StateHasChanged(); 145 | } 146 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/user/login" 2 | @inject Services.IUserService UserService 3 | @inject IAlertService AlertService 4 | @inject NavigationManager navManager 5 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 6 | 7 |
8 |
9 |

Login

10 |
11 | 12 | 13 |
14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 |
23 | 30 |
31 |
32 |
33 |
34 | 35 | @code { 36 | private Blazorcrud.Client.Shared.Login loginModel = new Blazorcrud.Client.Shared.Login(); 37 | private bool loading; 38 | 39 | private async void OnValidSubmit() 40 | { 41 | // reset alerts on submit 42 | AlertService.Clear(); 43 | 44 | loading = true; 45 | try 46 | { 47 | await UserService.Login(loginModel); 48 | var returnUrl = navManager.QueryString("returnUrl") ?? ""; 49 | if (returnUrl != string.Empty) 50 | { 51 | navManager.NavigateTo(returnUrl); 52 | } 53 | else{ 54 | if (PageHistoryState.CanGoBack()){ 55 | navManager.NavigateTo(PageHistoryState.GetGoBackPage()); 56 | } 57 | else{ 58 | navManager.NavigateTo("/user/1"); 59 | } 60 | } 61 | } 62 | catch (Exception ex) 63 | { 64 | AlertService.Error(ex.Message); 65 | loading = false; 66 | StateHasChanged(); 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/Logout.razor: -------------------------------------------------------------------------------- 1 | @page "/user/logout" 2 | @inject IUserService UserService 3 | 4 | @code { 5 | protected override async void OnInitialized() 6 | { 7 | await UserService.Logout(); 8 | } 9 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/Update.razor: -------------------------------------------------------------------------------- 1 | @page "/user/updateuser/{id}" 2 | @attribute [Authorize] 3 | @inject IAlertService AlertService 4 | @inject IUserService UserService 5 | @inject NavigationManager navManager 6 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 7 | 8 |

Edit User

9 |
10 | 11 | 13 | 14 | @code { 15 | bool loading = false; 16 | 17 | [Parameter] 18 | public string Id { get; set; } 19 | 20 | User user = new User(); 21 | 22 | protected async override Task OnParametersSetAsync() 23 | { 24 | user = await UserService.GetUser(int.Parse(Id)); 25 | } 26 | 27 | async Task UpdateUser() 28 | { 29 | loading = true; 30 | try 31 | { 32 | await UserService.UpdateUser(user); 33 | AlertService.Success("User updated successfully", keepAfterRouteChange: true); 34 | if (PageHistoryState.CanGoBack()){ 35 | navManager.NavigateTo(PageHistoryState.GetGoBackPage()); 36 | } 37 | else{ 38 | navManager.NavigateTo("/user/1"); 39 | } 40 | } 41 | catch (Exception ex) 42 | { 43 | AlertService.Error(ex.Message); 44 | loading = false; 45 | StateHasChanged(); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Pages/Users/View.razor: -------------------------------------------------------------------------------- 1 | @page "/user/viewuser/{id}" 2 | @inject IUserService UserService 3 | @inject NavigationManager navManager 4 | @inject Blazorcrud.Client.Shared.PageHistoryState PageHistoryState 5 | 6 |

View User

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
Id@user.Id
First Name@user.FirstName
Last Name@user.LastName
User Name@user.Username
29 | 30 |
31 | @if (PageHistoryState.CanGoBack()){ 32 | Back 33 | } 34 | else{ 35 | Back 36 | } 37 |
38 | 39 | @code { 40 | [Parameter] 41 | public string Id { get; set; } 42 | 43 | User user = new User(); 44 | 45 | protected async override Task OnParametersSetAsync() 46 | { 47 | user = await UserService.GetUser(int.Parse(Id)); 48 | } 49 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Client; 2 | using Blazorcrud.Client.Services; 3 | using Blazorcrud.Client.Shared; 4 | using Microsoft.AspNetCore.Components.Web; 5 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 6 | 7 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 8 | builder.RootComponents.Add("#app"); 9 | builder.RootComponents.Add("head::after"); 10 | builder.Services.AddScoped(); 11 | builder.Services.AddScoped(); 12 | builder.Services.AddScoped(); 13 | builder.Services.AddScoped(); 14 | builder.Services.AddScoped(); 15 | builder.Services.AddScoped(); 16 | builder.Services.AddScoped(x => { 17 | var apiUrl = new Uri("http://localhost:5001"); 18 | return new HttpClient() {BaseAddress = apiUrl}; 19 | }); 20 | builder.Services.AddSingleton(); 21 | 22 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); 23 | 24 | await builder.Build().RunAsync(); -------------------------------------------------------------------------------- /Blazorcrud.Client/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17056", 7 | "sslPort": 44331 8 | } 9 | }, 10 | "profiles": { 11 | "Blazorcrud": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Services/IPersonService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Data; 2 | using Blazorcrud.Shared.Models; 3 | 4 | namespace Blazorcrud.Client.Services 5 | { 6 | public interface IPersonService 7 | { 8 | Task> GetPeople(string name, string page); 9 | Task GetPerson(int id); 10 | 11 | Task DeletePerson(int id); 12 | 13 | Task AddPerson(Person person); 14 | 15 | Task UpdatePerson(Person person); 16 | } 17 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Services/IUploadService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Data; 2 | using Blazorcrud.Shared.Models; 3 | 4 | namespace Blazorcrud.Client.Services 5 | { 6 | public interface IUploadService 7 | { 8 | Task> GetUploads(string name, string page); 9 | Task GetUpload(int id); 10 | 11 | Task DeleteUpload(int id); 12 | 13 | Task AddUpload(Upload upload); 14 | 15 | Task UpdateUpload(Upload upload); 16 | } 17 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Services/IUserService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Client.Shared; 2 | using Blazorcrud.Shared.Data; 3 | using Blazorcrud.Shared.Models; 4 | 5 | namespace Blazorcrud.Client.Services 6 | { 7 | public interface IUserService 8 | { 9 | User User {get; } 10 | Task Initialize(); 11 | Task Login(Login model); 12 | Task Logout(); 13 | Task> GetUsers(string name, string page); 14 | Task GetUser(int id); 15 | Task DeleteUser(int id); 16 | Task AddUser(User user); 17 | Task UpdateUser(User user); 18 | } 19 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Services/PersonService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Client.Shared; 2 | using Blazorcrud.Shared.Data; 3 | using Blazorcrud.Shared.Models; 4 | using Microsoft.AspNetCore.Components; 5 | 6 | namespace Blazorcrud.Client.Services 7 | { 8 | public class PersonService: IPersonService 9 | { 10 | private IHttpService _httpService; 11 | 12 | public PersonService(IHttpService httpService) 13 | { 14 | _httpService = httpService; 15 | } 16 | 17 | public async Task> GetPeople(string name, string page) 18 | { 19 | return await _httpService.Get>("api/person" + "?page=" + page + "&name=" + name); 20 | } 21 | 22 | public async Task GetPerson(int id) 23 | { 24 | return await _httpService.Get($"api/person/{id}"); 25 | } 26 | 27 | public async Task DeletePerson(int id) 28 | { 29 | await _httpService.Delete($"api/person/{id}"); 30 | } 31 | 32 | public async Task AddPerson(Person person) 33 | { 34 | await _httpService.Post($"api/person", person); 35 | } 36 | 37 | public async Task UpdatePerson(Person person) 38 | { 39 | await _httpService.Put($"api/person", person); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Services/UploadService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Client.Shared; 2 | using Blazorcrud.Shared.Data; 3 | using Blazorcrud.Shared.Models; 4 | 5 | namespace Blazorcrud.Client.Services 6 | { 7 | public class UploadService: IUploadService 8 | { 9 | private readonly IHttpService _httpService; 10 | 11 | public UploadService(IHttpService httpService) 12 | { 13 | _httpService=httpService; 14 | } 15 | 16 | public async Task> GetUploads(string name, string page) 17 | { 18 | return await _httpService.Get>("api/upload" + "?page=" + page + "&name=" + name); 19 | } 20 | 21 | public async Task GetUpload(int id) 22 | { 23 | return await _httpService.Get($"api/upload/{id}"); 24 | } 25 | 26 | public async Task DeleteUpload(int id) 27 | { 28 | await _httpService.Delete($"api/upload/{id}"); 29 | } 30 | 31 | public async Task AddUpload(Upload upload) 32 | { 33 | await _httpService.Post($"api/upload", upload); 34 | } 35 | 36 | public async Task UpdateUpload(Upload upload) 37 | { 38 | await _httpService.Put($"api/upload", upload); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Services/UserService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Client.Shared; 2 | using Blazorcrud.Shared.Data; 3 | using Blazorcrud.Shared.Models; 4 | using Microsoft.AspNetCore.Components; 5 | 6 | namespace Blazorcrud.Client.Services 7 | { 8 | public class UserService: IUserService 9 | { 10 | private IHttpService _httpService; 11 | private ILocalStorageService _localStorageService; 12 | private NavigationManager _navigationManager; 13 | private string _userKey = "user"; 14 | 15 | public User User {get; private set;} 16 | 17 | public UserService(IHttpService httpService, ILocalStorageService localStorageService, NavigationManager navigationManager) 18 | { 19 | _httpService = httpService; 20 | _localStorageService = localStorageService; 21 | _navigationManager = navigationManager; 22 | } 23 | 24 | public async Task Initialize() 25 | { 26 | User = await _localStorageService.GetItem(_userKey); 27 | } 28 | 29 | public async Task Login(Login model) 30 | { 31 | User = await _httpService.Post("/api/user/authenticate", model); 32 | await _localStorageService.SetItem(_userKey, User); 33 | } 34 | 35 | public async Task Logout() 36 | { 37 | User = null; 38 | await _localStorageService.RemoveItem(_userKey); 39 | _navigationManager.NavigateTo("/user/login"); 40 | } 41 | 42 | public async Task> GetUsers(string name, string page) 43 | { 44 | return await _httpService.Get>("api/user" + "?page=" + page + "&name=" + name); 45 | } 46 | 47 | public async Task GetUser(int id) 48 | { 49 | return await _httpService.Get($"api/user/{id}"); 50 | } 51 | 52 | public async Task DeleteUser(int id) 53 | { 54 | await _httpService.Delete($"api/user/{id}"); 55 | // auto logout if the user deleted their own record 56 | if (id == User.Id) 57 | await Logout(); 58 | } 59 | 60 | public async Task AddUser(User user) 61 | { 62 | await _httpService.Post($"api/user", user); 63 | } 64 | 65 | public async Task UpdateUser(User user) 66 | { 67 | await _httpService.Put($"api/user", user); 68 | // update local storage if the user updated their own record 69 | if (user.Id == User.Id) 70 | { 71 | User.FirstName = user.FirstName; 72 | User.LastName = user.LastName; 73 | User.Username = user.Username; 74 | await _localStorageService.SetItem(_userKey, User); 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/Alert.razor: -------------------------------------------------------------------------------- 1 | @implements IDisposable 2 | @inject IAlertService AlertService 3 | @inject NavigationManager NavigationManager 4 | 5 | @foreach (var alert in alerts) 6 | { 7 |
8 | × 9 | @alert.Message 10 |
11 | } 12 | 13 | @code { 14 | [Parameter] 15 | public string Id { get; set; } = "default-alert"; 16 | 17 | [Parameter] 18 | public bool Fade { get; set; } = true; 19 | 20 | private List alerts = new List(); 21 | 22 | protected override void OnInitialized() 23 | { 24 | // subscribe to new alerts and location change events 25 | AlertService.OnAlert += OnAlert; 26 | NavigationManager.LocationChanged += OnLocationChange; 27 | } 28 | 29 | public void Dispose() 30 | { 31 | // unsubscribe from alerts and location change events 32 | AlertService.OnAlert -= OnAlert; 33 | NavigationManager.LocationChanged -= OnLocationChange; 34 | } 35 | 36 | private async void OnAlert(AlertModel alert) 37 | { 38 | // ignore alerts sent to other alert components 39 | if (alert.Id != Id) 40 | return; 41 | 42 | // clear alerts when an empty alert is received 43 | if (alert.Message == null) 44 | { 45 | // remove alerts without the 'KeepAfterRouteChange' flag set to true 46 | alerts.RemoveAll(x => !x.KeepAfterRouteChange); 47 | 48 | // set the 'KeepAfterRouteChange' flag to false for the 49 | // remaining alerts so they are removed on the next clear 50 | alerts.ForEach(x => x.KeepAfterRouteChange = false); 51 | } 52 | else 53 | { 54 | // add alert to array 55 | alerts.Add(alert); 56 | StateHasChanged(); 57 | 58 | // auto close alert if required 59 | if (alert.AutoClose) 60 | { 61 | await Task.Delay(3000); 62 | RemoveAlert(alert); 63 | } 64 | } 65 | 66 | StateHasChanged(); 67 | } 68 | 69 | private void OnLocationChange(object sender, LocationChangedEventArgs e) 70 | { 71 | AlertService.Clear(Id); 72 | } 73 | 74 | private async void RemoveAlert(AlertModel alert) 75 | { 76 | // check if already removed to prevent error on auto close 77 | if (!alerts.Contains(alert)) return; 78 | 79 | if (Fade) 80 | { 81 | // fade out alert 82 | alert.Fade = true; 83 | 84 | // remove alert after faded out 85 | await Task.Delay(250); 86 | alerts.Remove(alert); 87 | } 88 | else 89 | { 90 | // remove alert 91 | alerts.Remove(alert); 92 | } 93 | 94 | StateHasChanged(); 95 | } 96 | 97 | private string CssClass(AlertModel alert) 98 | { 99 | if (alert == null) return null; 100 | 101 | var classes = new List { "alert", "alert-dismissable", "mt-4", "container" }; 102 | 103 | var alertTypeClass = new Dictionary(); 104 | alertTypeClass[AlertType.Success] = "alert-success"; 105 | alertTypeClass[AlertType.Error] = "alert-danger"; 106 | alertTypeClass[AlertType.Info] = "alert-info"; 107 | alertTypeClass[AlertType.Warning] = "alert-warning"; 108 | 109 | classes.Add(alertTypeClass[alert.Type]); 110 | 111 | if (alert.Fade) 112 | classes.Add("fade"); 113 | 114 | return string.Join(' ', classes); 115 | } 116 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/AlertModel.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Client.Shared 2 | { 3 | public class AlertModel 4 | { 5 | public string Id { get; set; } 6 | public AlertType Type { get; set; } 7 | public string Message { get; set; } 8 | public bool AutoClose { get; set; } 9 | public bool KeepAfterRouteChange { get; set; } 10 | public bool Fade { get; set; } 11 | } 12 | 13 | public enum AlertType 14 | { 15 | Success, 16 | Error, 17 | Info, 18 | Warning 19 | } 20 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/AlertService.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Client.Shared 2 | { 3 | public interface IAlertService 4 | { 5 | event Action OnAlert; 6 | void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true); 7 | void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true); 8 | void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true); 9 | void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true); 10 | void Alert(AlertModel alert); 11 | void Clear(string id = null); 12 | } 13 | 14 | public class AlertService : IAlertService 15 | { 16 | private const string _defaultId = "default-alert"; 17 | public event Action OnAlert; 18 | 19 | public void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true) 20 | { 21 | this.Alert(new AlertModel 22 | { 23 | Type = AlertType.Success, 24 | Message = message, 25 | KeepAfterRouteChange = keepAfterRouteChange, 26 | AutoClose = autoClose 27 | }); 28 | } 29 | 30 | public void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true) 31 | { 32 | this.Alert(new AlertModel 33 | { 34 | Type = AlertType.Error, 35 | Message = message, 36 | KeepAfterRouteChange = keepAfterRouteChange, 37 | AutoClose = autoClose 38 | }); 39 | } 40 | 41 | public void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true) 42 | { 43 | this.Alert(new AlertModel 44 | { 45 | Type = AlertType.Info, 46 | Message = message, 47 | KeepAfterRouteChange = keepAfterRouteChange, 48 | AutoClose = autoClose 49 | }); 50 | } 51 | 52 | public void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true) 53 | { 54 | this.Alert(new AlertModel 55 | { 56 | Type = AlertType.Warning, 57 | Message = message, 58 | KeepAfterRouteChange = keepAfterRouteChange, 59 | AutoClose = autoClose 60 | }); 61 | } 62 | 63 | public void Alert(AlertModel alert) 64 | { 65 | alert.Id = alert.Id ?? _defaultId; 66 | this.OnAlert?.Invoke(alert); 67 | } 68 | 69 | public void Clear(string id = _defaultId) 70 | { 71 | this.OnAlert?.Invoke(new AlertModel { Id = id }); 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/AppRouteView.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Client.Services; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Components.Rendering; 5 | using System.Net; 6 | 7 | namespace Blazorcrud.Client.Shared 8 | { 9 | public class AppRouteView : RouteView 10 | { 11 | [Inject] 12 | public NavigationManager NavigationManager { get; set; } 13 | 14 | [Inject] 15 | public IUserService UserService { get; set; } 16 | 17 | protected override void Render(RenderTreeBuilder builder) 18 | { 19 | var authorize = Attribute.GetCustomAttribute(RouteData.PageType, typeof(AuthorizeAttribute)) != null; 20 | if (authorize && UserService.User == null) 21 | { 22 | var returnUrl = WebUtility.UrlEncode(new Uri(NavigationManager.Uri).PathAndQuery); 23 | NavigationManager.NavigateTo($"user/login?returnUrl={returnUrl}"); 24 | } 25 | else 26 | { 27 | base.Render(builder); 28 | } 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/FluentValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Components.Forms; 4 | 5 | namespace Blazorcrud.Client.Shared 6 | { 7 | public class FluentValidator : ComponentBase where TValidator : IValidator, new() 8 | { 9 | private readonly static char[] separators = new[] { '.', '[' }; 10 | private TValidator validator; 11 | 12 | [CascadingParameter] private EditContext EditContext { get; set; } 13 | 14 | protected override void OnInitialized() 15 | { 16 | validator = new TValidator(); 17 | var messages = new ValidationMessageStore(EditContext); 18 | 19 | // Validate on field changes only after initial submission 20 | // Check Saintly use of eventArgs.FieldIdentifier and ValidateField method 21 | EditContext.OnFieldChanged += (sender, eventArgs) 22 | => ValidateModel((EditContext)sender, messages); 23 | 24 | // Validate when the entire form requests submission 25 | EditContext.OnValidationRequested += (sender, eventArgs) 26 | => ValidateModel((EditContext)sender, messages); 27 | } 28 | 29 | private void ValidateModel(EditContext editContext, ValidationMessageStore messages) 30 | { 31 | var context = new ValidationContext(editContext.Model); 32 | var validationResult = validator.Validate(context); 33 | messages.Clear(); 34 | foreach (var error in validationResult.Errors) 35 | { 36 | var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName); 37 | messages.Add(fieldIdentifier, error.ErrorMessage); 38 | } 39 | editContext.NotifyValidationStateChanged(); 40 | } 41 | 42 | private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath) 43 | { 44 | // This method parses property paths like 'SomeProp.MyCollection[123].ChildProp' 45 | // and returns a FieldIdentifier which is an (instance, propName) pair. For example, 46 | // it would return the pair (SomeProp.MyCollection[123], "ChildProp"). It traverses 47 | // as far into the propertyPath as it can go until it finds any null instance. 48 | 49 | var obj = editContext.Model; 50 | 51 | while (true) 52 | { 53 | var nextTokenEnd = propertyPath.IndexOfAny(separators); 54 | if (nextTokenEnd < 0) 55 | { 56 | return new FieldIdentifier(obj, propertyPath); 57 | } 58 | 59 | var nextToken = propertyPath.Substring(0, nextTokenEnd); 60 | propertyPath = propertyPath.Substring(nextTokenEnd + 1); 61 | 62 | object newObj; 63 | if (nextToken.EndsWith("]")) 64 | { 65 | // It's an indexer 66 | // This code assumes C# conventions (one indexer named Item with one param) 67 | nextToken = nextToken.Substring(0, nextToken.Length - 1); 68 | var prop = obj.GetType().GetProperty("Item"); 69 | var indexerType = prop.GetIndexParameters()[0].ParameterType; 70 | var indexerValue = Convert.ChangeType(nextToken, indexerType); 71 | newObj = prop.GetValue(obj, new object[] { indexerValue }); 72 | } 73 | else 74 | { 75 | // It's a regular property 76 | var prop = obj.GetType().GetProperty(nextToken); 77 | if (prop == null) 78 | { 79 | throw new InvalidOperationException($"Could not find property named {nextToken} on object of type {obj.GetType().FullName}."); 80 | } 81 | newObj = prop.GetValue(obj); 82 | } 83 | 84 | if (newObj == null) 85 | { 86 | // This is as far as we can go 87 | return new FieldIdentifier(obj, nextToken); 88 | } 89 | 90 | obj = newObj; 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/HttpService.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using System.Net; 4 | using System.Net.Http.Headers; 5 | using System.Net.Http.Json; 6 | using System.Text; 7 | using System.Text.Json; 8 | 9 | namespace Blazorcrud.Client.Shared 10 | { 11 | public interface IHttpService 12 | { 13 | Task Get(string uri); 14 | Task Post(string uri, object value); 15 | Task Post(string uri, object value); 16 | Task Put(string uri, object value); 17 | Task Put(string uri, object value); 18 | Task Delete(string uri); 19 | Task Delete(string uri); 20 | } 21 | 22 | public class HttpService : IHttpService 23 | { 24 | private HttpClient _httpClient; 25 | private NavigationManager _navigationManager; 26 | private ILocalStorageService _localStorageService; 27 | private IConfiguration _configuration; 28 | 29 | public HttpService( 30 | HttpClient httpClient, 31 | NavigationManager navigationManager, 32 | ILocalStorageService localStorageService, 33 | IConfiguration configuration 34 | ) { 35 | _httpClient = httpClient; 36 | _navigationManager = navigationManager; 37 | _localStorageService = localStorageService; 38 | _configuration = configuration; 39 | } 40 | 41 | public async Task Get(string uri) 42 | { 43 | var request = new HttpRequestMessage(HttpMethod.Get, uri); 44 | return await sendRequest(request); 45 | } 46 | 47 | public async Task Post(string uri, object value) 48 | { 49 | var request = createRequest(HttpMethod.Post, uri, value); 50 | await sendRequest(request); 51 | } 52 | 53 | public async Task Post(string uri, object value) 54 | { 55 | var request = createRequest(HttpMethod.Post, uri, value); 56 | return await sendRequest(request); 57 | } 58 | 59 | public async Task Put(string uri, object value) 60 | { 61 | var request = createRequest(HttpMethod.Put, uri, value); 62 | await sendRequest(request); 63 | } 64 | 65 | public async Task Put(string uri, object value) 66 | { 67 | var request = createRequest(HttpMethod.Put, uri, value); 68 | return await sendRequest(request); 69 | } 70 | 71 | public async Task Delete(string uri) 72 | { 73 | var request = createRequest(HttpMethod.Delete, uri); 74 | await sendRequest(request); 75 | } 76 | 77 | public async Task Delete(string uri) 78 | { 79 | var request = createRequest(HttpMethod.Delete, uri); 80 | return await sendRequest(request); 81 | } 82 | 83 | // helper methods 84 | 85 | private HttpRequestMessage createRequest(HttpMethod method, string uri, object value = null) 86 | { 87 | var request = new HttpRequestMessage(method, uri); 88 | if (value != null) 89 | request.Content = new StringContent(JsonSerializer.Serialize(value), Encoding.UTF8, "application/json"); 90 | return request; 91 | } 92 | 93 | private async Task sendRequest(HttpRequestMessage request) 94 | { 95 | await addJwtHeader(request); 96 | 97 | // send request 98 | using var response = await _httpClient.SendAsync(request); 99 | 100 | // auto logout on 401 response 101 | if (response.StatusCode == HttpStatusCode.Unauthorized) 102 | { 103 | _navigationManager.NavigateTo("user/logout"); 104 | return; 105 | } 106 | 107 | await handleErrors(response); 108 | } 109 | 110 | private async Task sendRequest(HttpRequestMessage request) 111 | { 112 | await addJwtHeader(request); 113 | 114 | // send request 115 | using var response = await _httpClient.SendAsync(request); 116 | 117 | // auto logout on 401 response 118 | if (response.StatusCode == HttpStatusCode.Unauthorized) 119 | { 120 | _navigationManager.NavigateTo("user/logout"); 121 | return default; 122 | } 123 | 124 | await handleErrors(response); 125 | 126 | var options = new JsonSerializerOptions(); 127 | options.PropertyNameCaseInsensitive = true; 128 | options.Converters.Add(new StringConverter()); 129 | return await response.Content.ReadFromJsonAsync(options); 130 | } 131 | 132 | private async Task addJwtHeader(HttpRequestMessage request) 133 | { 134 | // add jwt auth header if user is logged in and request is to the api url 135 | var user = await _localStorageService.GetItem("user"); 136 | var isApiUrl = !request.RequestUri.IsAbsoluteUri; 137 | if (user != null && isApiUrl) 138 | request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", user.Token); 139 | } 140 | 141 | private async Task handleErrors(HttpResponseMessage response) 142 | { 143 | // throw exception on error response 144 | if (!response.IsSuccessStatusCode) 145 | { 146 | var error = await response.Content.ReadFromJsonAsync>(); 147 | throw new Exception(error["message"]); 148 | } 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/LocalStorageService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System.Text.Json; 3 | 4 | namespace Blazorcrud.Client.Shared 5 | { 6 | public interface ILocalStorageService 7 | { 8 | Task GetItem(string key); 9 | Task SetItem(string key, T value); 10 | Task RemoveItem(string key); 11 | } 12 | 13 | public class LocalStorageService : ILocalStorageService 14 | { 15 | private IJSRuntime _jsRuntime; 16 | 17 | public LocalStorageService(IJSRuntime jsRuntime) 18 | { 19 | _jsRuntime = jsRuntime; 20 | } 21 | 22 | public async Task GetItem(string key) 23 | { 24 | var json = await _jsRuntime.InvokeAsync("localStorage.getItem", key); 25 | 26 | if (json == null) 27 | return default; 28 | 29 | return JsonSerializer.Deserialize(json); 30 | } 31 | 32 | public async Task SetItem(string key, T value) 33 | { 34 | await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value)); 35 | } 36 | 37 | public async Task RemoveItem(string key) 38 | { 39 | await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/Login.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Blazorcrud.Client.Shared 4 | { 5 | public class Login 6 | { 7 | [Required] 8 | public string Username { get; set; } 9 | 10 | [Required] 11 | public string Password { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @inject IUserService UserService 3 | 4 | 24 | 25 |
26 |
27 |
28 | 29 | @Body 30 |
31 |
32 |
33 |
34 | © Beckshome.com 2024 35 |
36 |
37 |
38 | 39 | @code { 40 | public bool LoggedIn 41 | { 42 | get {return UserService.User != null;} 43 | } 44 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/PageHistoryState.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Client.Shared 2 | { 3 | public class PageHistoryState 4 | { 5 | private List previousPages; 6 | 7 | public PageHistoryState() 8 | { 9 | previousPages = new List(); 10 | } 11 | 12 | public void AddPageToHistory(string PageName) 13 | { 14 | previousPages.Add(PageName); 15 | } 16 | 17 | public string GetGoBackPage() 18 | { 19 | if (previousPages.Count > 1) 20 | { 21 | // page added on initialization, return second from last 22 | return previousPages.ElementAt(previousPages.Count - 1); 23 | } 24 | // can't go back page 25 | return previousPages.FirstOrDefault(); 26 | } 27 | 28 | public bool CanGoBack() 29 | { 30 | return previousPages.Count > 1; 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/Pager.razor: -------------------------------------------------------------------------------- 1 | @if (Result != null) 2 | { 3 |
4 |
5 | @if (Result.PageCount > 1) 6 | { 7 |
    8 |
  • 9 | @for (var i = StartIndex; i <= FinishIndex; i++) 10 | { 11 | var currentIndex = i; 12 | @if (i == Result.CurrentPage) 13 | { 14 |
  • @i
  • 15 | } 16 | else 17 | { 18 |
  • 19 | } 20 | } 21 |
  • 22 |
  • Page @Result.CurrentPage of @Result.PageCount
  • 23 |
24 | } 25 |
26 |
27 | } 28 | 29 | @code { 30 | [Parameter] 31 | public PagedResultBase Result { get; set; } 32 | 33 | [Parameter] 34 | public Action PageChanged { get; set; } 35 | 36 | protected int StartIndex { get; private set; } = 0; 37 | protected int FinishIndex { get; private set; } = 0; 38 | 39 | protected override async Task OnParametersSetAsync() 40 | { 41 | await base.OnParametersSetAsync(); 42 | StartIndex = Math.Max(Result.CurrentPage - 5, 1); 43 | FinishIndex = Math.Min(Result.CurrentPage + 5, Result.PageCount); 44 | } 45 | 46 | protected void PagerButtonClicked(int page) 47 | { 48 | PageChanged?.Invoke(page); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/Pager.razor.css: -------------------------------------------------------------------------------- 1 | /* Pagination Style */ 2 | .pagination .btn { 3 | background-color: darkgray; 4 | } 5 | 6 | .pagination span.btn { 7 | background-color: #000; 8 | color: #fff; 9 | } 10 | 11 | .pagination li { 12 | padding: 5px; 13 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/QueryExtensionMethods.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using System.Collections.Specialized; 3 | using System.Web; 4 | 5 | namespace Blazorcrud.Client.Shared 6 | { 7 | public static class ExtensionMethods 8 | { 9 | public static NameValueCollection QueryString(this NavigationManager navigationManager) 10 | { 11 | return HttpUtility.ParseQueryString(new Uri(navigationManager.Uri).Query); 12 | } 13 | 14 | public static string QueryString(this NavigationManager navigationManager, string key) 15 | { 16 | return navigationManager.QueryString()[key]; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/Shared/StringConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Blazorcrud.Client.Shared 5 | { 6 | public class StringConverter : JsonConverter 7 | { 8 | public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | // deserialize numbers as strings. 11 | if (reader.TokenType == JsonTokenType.Number) 12 | { 13 | return reader.GetInt32().ToString(); 14 | } 15 | else if (reader.TokenType == JsonTokenType.String) 16 | { 17 | return reader.GetString(); 18 | } 19 | 20 | throw new System.Text.Json.JsonException(); 21 | } 22 | 23 | public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) 24 | { 25 | writer.WriteStringValue(value); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /Blazorcrud.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using Blazorcrud.Client 2 | @using Blazorcrud.Client.Services 3 | @using Blazorcrud.Client.Shared 4 | @using Blazorcrud.Shared.Data 5 | @using Blazorcrud.Shared.Models 6 | @using Microsoft.AspNetCore.Authorization 7 | @using Microsoft.AspNetCore.Components.Forms 8 | @using Microsoft.AspNetCore.Components.Routing 9 | @using Microsoft.AspNetCore.Components.Web 10 | @using Microsoft.AspNetCore.Components.Web.Virtualization 11 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 12 | @using Microsoft.JSInterop 13 | @using System.Net.Http 14 | @using System.Net.Http.Json -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/app.css: -------------------------------------------------------------------------------- 1 | @import url('open-iconic/font/css/open-iconic-bootstrap.min.css'); 2 | 3 | html, body { 4 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 5 | } 6 | 7 | h1:focus { 8 | outline: none; 9 | } 10 | 11 | a, .btn-link { 12 | color: #0071c1; 13 | } 14 | 15 | .btn-primary { 16 | color: #fff; 17 | background-color: #1b6ec2; 18 | border-color: #1861ac; 19 | } 20 | 21 | .content { 22 | padding-top: 1.1rem; 23 | } 24 | 25 | .valid.modified:not([type=checkbox]) { 26 | outline: 1px solid #26b050; 27 | } 28 | 29 | .invalid { 30 | outline: 1px solid red; 31 | } 32 | 33 | .validation-message { 34 | color: red; 35 | } 36 | 37 | /* For add, edit, delete icons ------ */ 38 | .nounderline { 39 | text-decoration: none !important 40 | } 41 | 42 | /* Sticky footer styles------ */ 43 | html { 44 | position: relative; 45 | min-height: 100%; 46 | } 47 | 48 | body { 49 | margin-bottom: 60px; /* Margin bottom by footer height */ 50 | } 51 | 52 | .footer { 53 | position: absolute; 54 | bottom: 0; 55 | width: 100%; 56 | height: 60px; /* Set the fixed height of the footer here */ 57 | line-height: 60px; /* Vertically center the text there */ 58 | background-color: #f5f5f5; 59 | } 60 | 61 | a {cursor: pointer} 62 | 63 | .app-container { 64 | min-height: 320px; 65 | overflow: hidden; 66 | } 67 | 68 | #blazor-error-ui { 69 | background: lightyellow; 70 | bottom: 0; 71 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 72 | display: none; 73 | left: 0; 74 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 75 | position: fixed; 76 | width: 100%; 77 | z-index: 1000; 78 | } 79 | 80 | #blazor-error-ui .dismiss { 81 | cursor: pointer; 82 | position: absolute; 83 | right: 0.75rem; 84 | top: 0.5rem; 85 | } 86 | 87 | .blazor-error-boundary { 88 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 89 | padding: 1rem 1rem 1rem 3.7rem; 90 | color: white; 91 | } 92 | 93 | .blazor-error-boundary::after { 94 | content: "An error has occurred." 95 | } 96 | -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/FONT-LICENSE: -------------------------------------------------------------------------------- 1 | SIL OPEN FONT LICENSE Version 1.1 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | PREAMBLE 6 | The goals of the Open Font License (OFL) are to stimulate worldwide 7 | development of collaborative font projects, to support the font creation 8 | efforts of academic and linguistic communities, and to provide a free and 9 | open framework in which fonts may be shared and improved in partnership 10 | with others. 11 | 12 | The OFL allows the licensed fonts to be used, studied, modified and 13 | redistributed freely as long as they are not sold by themselves. The 14 | fonts, including any derivative works, can be bundled, embedded, 15 | redistributed and/or sold with any software provided that any reserved 16 | names are not used by derivative works. The fonts and derivatives, 17 | however, cannot be released under any other type of license. The 18 | requirement for fonts to remain under this license does not apply 19 | to any document created using the fonts or their derivatives. 20 | 21 | DEFINITIONS 22 | "Font Software" refers to the set of files released by the Copyright 23 | Holder(s) under this license and clearly marked as such. This may 24 | include source files, build scripts and documentation. 25 | 26 | "Reserved Font Name" refers to any names specified as such after the 27 | copyright statement(s). 28 | 29 | "Original Version" refers to the collection of Font Software components as 30 | distributed by the Copyright Holder(s). 31 | 32 | "Modified Version" refers to any derivative made by adding to, deleting, 33 | or substituting -- in part or in whole -- any of the components of the 34 | Original Version, by changing formats or by porting the Font Software to a 35 | new environment. 36 | 37 | "Author" refers to any designer, engineer, programmer, technical 38 | writer or other person who contributed to the Font Software. 39 | 40 | PERMISSION & CONDITIONS 41 | Permission is hereby granted, free of charge, to any person obtaining 42 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 43 | redistribute, and sell modified and unmodified copies of the Font 44 | Software, subject to the following conditions: 45 | 46 | 1) Neither the Font Software nor any of its individual components, 47 | in Original or Modified Versions, may be sold by itself. 48 | 49 | 2) Original or Modified Versions of the Font Software may be bundled, 50 | redistributed and/or sold with any software, provided that each copy 51 | contains the above copyright notice and this license. These can be 52 | included either as stand-alone text files, human-readable headers or 53 | in the appropriate machine-readable metadata fields within text or 54 | binary files as long as those fields can be easily viewed by the user. 55 | 56 | 3) No Modified Version of the Font Software may use the Reserved Font 57 | Name(s) unless explicit written permission is granted by the corresponding 58 | Copyright Holder. This restriction only applies to the primary font name as 59 | presented to the users. 60 | 61 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 62 | Software shall not be used to promote, endorse or advertise any 63 | Modified Version, except to acknowledge the contribution(s) of the 64 | Copyright Holder(s) and the Author(s) or with their explicit written 65 | permission. 66 | 67 | 5) The Font Software, modified or unmodified, in part or in whole, 68 | must be distributed entirely under this license, and must not be 69 | distributed under any other license. The requirement for fonts to 70 | remain under this license does not apply to any document created 71 | using the Font Software. 72 | 73 | TERMINATION 74 | This license becomes null and void if any of the above conditions are 75 | not met. 76 | 77 | DISCLAIMER 78 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 79 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 80 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 81 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 82 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 83 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 84 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 85 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 86 | OTHER DEALINGS IN THE FONT SOFTWARE. 87 | -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/ICON-LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Waybury 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/README.md: -------------------------------------------------------------------------------- 1 | [Open Iconic v1.1.1](http://useiconic.com/open) 2 | =========== 3 | 4 | ### Open Iconic is the open source sibling of [Iconic](http://useiconic.com). It is a hyper-legible collection of 223 icons with a tiny footprint—ready to use with Bootstrap and Foundation. [View the collection](http://useiconic.com/open#icons) 5 | 6 | 7 | 8 | ## What's in Open Iconic? 9 | 10 | * 223 icons designed to be legible down to 8 pixels 11 | * Super-light SVG files - 61.8 for the entire set 12 | * SVG sprite—the modern replacement for icon fonts 13 | * Webfont (EOT, OTF, SVG, TTF, WOFF), PNG and WebP formats 14 | * Webfont stylesheets (including versions for Bootstrap and Foundation) in CSS, LESS, SCSS and Stylus formats 15 | * PNG and WebP raster images in 8px, 16px, 24px, 32px, 48px and 64px. 16 | 17 | 18 | ## Getting Started 19 | 20 | #### For code samples and everything else you need to get started with Open Iconic, check out our [Icons](http://useiconic.com/open#icons) and [Reference](http://useiconic.com/open#reference) sections. 21 | 22 | ### General Usage 23 | 24 | #### Using Open Iconic's SVGs 25 | 26 | We like SVGs and we think they're the way to display icons on the web. Since Open Iconic are just basic SVGs, we suggest you display them like you would any other image (don't forget the `alt` attribute). 27 | 28 | ``` 29 | icon name 30 | ``` 31 | 32 | #### Using Open Iconic's SVG Sprite 33 | 34 | Open Iconic also comes in a SVG sprite which allows you to display all the icons in the set with a single request. It's like an icon font, without being a hack. 35 | 36 | Adding an icon from an SVG sprite is a little different than what you're used to, but it's still a piece of cake. *Tip: To make your icons easily style able, we suggest adding a general class to the* `` *tag and a unique class name for each different icon in the* `` *tag.* 37 | 38 | ``` 39 | 40 | 41 | 42 | ``` 43 | 44 | Sizing icons only needs basic CSS. All the icons are in a square format, so just set the `` tag with equal width and height dimensions. 45 | 46 | ``` 47 | .icon { 48 | width: 16px; 49 | height: 16px; 50 | } 51 | ``` 52 | 53 | Coloring icons is even easier. All you need to do is set the `fill` rule on the `` tag. 54 | 55 | ``` 56 | .icon-account-login { 57 | fill: #f00; 58 | } 59 | ``` 60 | 61 | To learn more about SVG Sprites, read [Chris Coyier's guide](http://css-tricks.com/svg-sprites-use-better-icon-fonts/). 62 | 63 | #### Using Open Iconic's Icon Font... 64 | 65 | 66 | ##### …with Bootstrap 67 | 68 | You can find our Bootstrap stylesheets in `font/css/open-iconic-bootstrap.{css, less, scss, styl}` 69 | 70 | 71 | ``` 72 | 73 | ``` 74 | 75 | 76 | ``` 77 | 78 | ``` 79 | 80 | ##### …with Foundation 81 | 82 | You can find our Foundation stylesheets in `font/css/open-iconic-foundation.{css, less, scss, styl}` 83 | 84 | ``` 85 | 86 | ``` 87 | 88 | 89 | ``` 90 | 91 | ``` 92 | 93 | ##### …on its own 94 | 95 | You can find our default stylesheets in `font/css/open-iconic.{css, less, scss, styl}` 96 | 97 | ``` 98 | 99 | ``` 100 | 101 | ``` 102 | 103 | ``` 104 | 105 | 106 | ## License 107 | 108 | ### Icons 109 | 110 | All code (including SVG markup) is under the [MIT License](http://opensource.org/licenses/MIT). 111 | 112 | ### Fonts 113 | 114 | All fonts are under the [SIL Licensed](http://scripts.sil.org/cms/scripts/page.php?item_id=OFL_web). 115 | -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/font/css/open-iconic-bootstrap.min.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:Icons;src:url(../fonts/open-iconic.eot);src:url(../fonts/open-iconic.eot?#iconic-sm) format('embedded-opentype'),url(../fonts/open-iconic.woff) format('woff'),url(../fonts/open-iconic.ttf) format('truetype'),url(../fonts/open-iconic.otf) format('opentype'),url(../fonts/open-iconic.svg#iconic-sm) format('svg');font-weight:400;font-style:normal}.oi{position:relative;top:1px;display:inline-block;speak:none;font-family:Icons;font-style:normal;font-weight:400;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.oi:empty:before{width:1em;text-align:center;box-sizing:content-box}.oi.oi-align-center:before{text-align:center}.oi.oi-align-left:before{text-align:left}.oi.oi-align-right:before{text-align:right}.oi.oi-flip-horizontal:before{-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.oi.oi-flip-vertical:before{-webkit-transform:scale(1,-1);-ms-transform:scale(-1,1);transform:scale(1,-1)}.oi.oi-flip-horizontal-vertical:before{-webkit-transform:scale(-1,-1);-ms-transform:scale(-1,1);transform:scale(-1,-1)}.oi-account-login:before{content:'\e000'}.oi-account-logout:before{content:'\e001'}.oi-action-redo:before{content:'\e002'}.oi-action-undo:before{content:'\e003'}.oi-align-center:before{content:'\e004'}.oi-align-left:before{content:'\e005'}.oi-align-right:before{content:'\e006'}.oi-aperture:before{content:'\e007'}.oi-arrow-bottom:before{content:'\e008'}.oi-arrow-circle-bottom:before{content:'\e009'}.oi-arrow-circle-left:before{content:'\e00a'}.oi-arrow-circle-right:before{content:'\e00b'}.oi-arrow-circle-top:before{content:'\e00c'}.oi-arrow-left:before{content:'\e00d'}.oi-arrow-right:before{content:'\e00e'}.oi-arrow-thick-bottom:before{content:'\e00f'}.oi-arrow-thick-left:before{content:'\e010'}.oi-arrow-thick-right:before{content:'\e011'}.oi-arrow-thick-top:before{content:'\e012'}.oi-arrow-top:before{content:'\e013'}.oi-audio-spectrum:before{content:'\e014'}.oi-audio:before{content:'\e015'}.oi-badge:before{content:'\e016'}.oi-ban:before{content:'\e017'}.oi-bar-chart:before{content:'\e018'}.oi-basket:before{content:'\e019'}.oi-battery-empty:before{content:'\e01a'}.oi-battery-full:before{content:'\e01b'}.oi-beaker:before{content:'\e01c'}.oi-bell:before{content:'\e01d'}.oi-bluetooth:before{content:'\e01e'}.oi-bold:before{content:'\e01f'}.oi-bolt:before{content:'\e020'}.oi-book:before{content:'\e021'}.oi-bookmark:before{content:'\e022'}.oi-box:before{content:'\e023'}.oi-briefcase:before{content:'\e024'}.oi-british-pound:before{content:'\e025'}.oi-browser:before{content:'\e026'}.oi-brush:before{content:'\e027'}.oi-bug:before{content:'\e028'}.oi-bullhorn:before{content:'\e029'}.oi-calculator:before{content:'\e02a'}.oi-calendar:before{content:'\e02b'}.oi-camera-slr:before{content:'\e02c'}.oi-caret-bottom:before{content:'\e02d'}.oi-caret-left:before{content:'\e02e'}.oi-caret-right:before{content:'\e02f'}.oi-caret-top:before{content:'\e030'}.oi-cart:before{content:'\e031'}.oi-chat:before{content:'\e032'}.oi-check:before{content:'\e033'}.oi-chevron-bottom:before{content:'\e034'}.oi-chevron-left:before{content:'\e035'}.oi-chevron-right:before{content:'\e036'}.oi-chevron-top:before{content:'\e037'}.oi-circle-check:before{content:'\e038'}.oi-circle-x:before{content:'\e039'}.oi-clipboard:before{content:'\e03a'}.oi-clock:before{content:'\e03b'}.oi-cloud-download:before{content:'\e03c'}.oi-cloud-upload:before{content:'\e03d'}.oi-cloud:before{content:'\e03e'}.oi-cloudy:before{content:'\e03f'}.oi-code:before{content:'\e040'}.oi-cog:before{content:'\e041'}.oi-collapse-down:before{content:'\e042'}.oi-collapse-left:before{content:'\e043'}.oi-collapse-right:before{content:'\e044'}.oi-collapse-up:before{content:'\e045'}.oi-command:before{content:'\e046'}.oi-comment-square:before{content:'\e047'}.oi-compass:before{content:'\e048'}.oi-contrast:before{content:'\e049'}.oi-copywriting:before{content:'\e04a'}.oi-credit-card:before{content:'\e04b'}.oi-crop:before{content:'\e04c'}.oi-dashboard:before{content:'\e04d'}.oi-data-transfer-download:before{content:'\e04e'}.oi-data-transfer-upload:before{content:'\e04f'}.oi-delete:before{content:'\e050'}.oi-dial:before{content:'\e051'}.oi-document:before{content:'\e052'}.oi-dollar:before{content:'\e053'}.oi-double-quote-sans-left:before{content:'\e054'}.oi-double-quote-sans-right:before{content:'\e055'}.oi-double-quote-serif-left:before{content:'\e056'}.oi-double-quote-serif-right:before{content:'\e057'}.oi-droplet:before{content:'\e058'}.oi-eject:before{content:'\e059'}.oi-elevator:before{content:'\e05a'}.oi-ellipses:before{content:'\e05b'}.oi-envelope-closed:before{content:'\e05c'}.oi-envelope-open:before{content:'\e05d'}.oi-euro:before{content:'\e05e'}.oi-excerpt:before{content:'\e05f'}.oi-expand-down:before{content:'\e060'}.oi-expand-left:before{content:'\e061'}.oi-expand-right:before{content:'\e062'}.oi-expand-up:before{content:'\e063'}.oi-external-link:before{content:'\e064'}.oi-eye:before{content:'\e065'}.oi-eyedropper:before{content:'\e066'}.oi-file:before{content:'\e067'}.oi-fire:before{content:'\e068'}.oi-flag:before{content:'\e069'}.oi-flash:before{content:'\e06a'}.oi-folder:before{content:'\e06b'}.oi-fork:before{content:'\e06c'}.oi-fullscreen-enter:before{content:'\e06d'}.oi-fullscreen-exit:before{content:'\e06e'}.oi-globe:before{content:'\e06f'}.oi-graph:before{content:'\e070'}.oi-grid-four-up:before{content:'\e071'}.oi-grid-three-up:before{content:'\e072'}.oi-grid-two-up:before{content:'\e073'}.oi-hard-drive:before{content:'\e074'}.oi-header:before{content:'\e075'}.oi-headphones:before{content:'\e076'}.oi-heart:before{content:'\e077'}.oi-home:before{content:'\e078'}.oi-image:before{content:'\e079'}.oi-inbox:before{content:'\e07a'}.oi-infinity:before{content:'\e07b'}.oi-info:before{content:'\e07c'}.oi-italic:before{content:'\e07d'}.oi-justify-center:before{content:'\e07e'}.oi-justify-left:before{content:'\e07f'}.oi-justify-right:before{content:'\e080'}.oi-key:before{content:'\e081'}.oi-laptop:before{content:'\e082'}.oi-layers:before{content:'\e083'}.oi-lightbulb:before{content:'\e084'}.oi-link-broken:before{content:'\e085'}.oi-link-intact:before{content:'\e086'}.oi-list-rich:before{content:'\e087'}.oi-list:before{content:'\e088'}.oi-location:before{content:'\e089'}.oi-lock-locked:before{content:'\e08a'}.oi-lock-unlocked:before{content:'\e08b'}.oi-loop-circular:before{content:'\e08c'}.oi-loop-square:before{content:'\e08d'}.oi-loop:before{content:'\e08e'}.oi-magnifying-glass:before{content:'\e08f'}.oi-map-marker:before{content:'\e090'}.oi-map:before{content:'\e091'}.oi-media-pause:before{content:'\e092'}.oi-media-play:before{content:'\e093'}.oi-media-record:before{content:'\e094'}.oi-media-skip-backward:before{content:'\e095'}.oi-media-skip-forward:before{content:'\e096'}.oi-media-step-backward:before{content:'\e097'}.oi-media-step-forward:before{content:'\e098'}.oi-media-stop:before{content:'\e099'}.oi-medical-cross:before{content:'\e09a'}.oi-menu:before{content:'\e09b'}.oi-microphone:before{content:'\e09c'}.oi-minus:before{content:'\e09d'}.oi-monitor:before{content:'\e09e'}.oi-moon:before{content:'\e09f'}.oi-move:before{content:'\e0a0'}.oi-musical-note:before{content:'\e0a1'}.oi-paperclip:before{content:'\e0a2'}.oi-pencil:before{content:'\e0a3'}.oi-people:before{content:'\e0a4'}.oi-person:before{content:'\e0a5'}.oi-phone:before{content:'\e0a6'}.oi-pie-chart:before{content:'\e0a7'}.oi-pin:before{content:'\e0a8'}.oi-play-circle:before{content:'\e0a9'}.oi-plus:before{content:'\e0aa'}.oi-power-standby:before{content:'\e0ab'}.oi-print:before{content:'\e0ac'}.oi-project:before{content:'\e0ad'}.oi-pulse:before{content:'\e0ae'}.oi-puzzle-piece:before{content:'\e0af'}.oi-question-mark:before{content:'\e0b0'}.oi-rain:before{content:'\e0b1'}.oi-random:before{content:'\e0b2'}.oi-reload:before{content:'\e0b3'}.oi-resize-both:before{content:'\e0b4'}.oi-resize-height:before{content:'\e0b5'}.oi-resize-width:before{content:'\e0b6'}.oi-rss-alt:before{content:'\e0b7'}.oi-rss:before{content:'\e0b8'}.oi-script:before{content:'\e0b9'}.oi-share-boxed:before{content:'\e0ba'}.oi-share:before{content:'\e0bb'}.oi-shield:before{content:'\e0bc'}.oi-signal:before{content:'\e0bd'}.oi-signpost:before{content:'\e0be'}.oi-sort-ascending:before{content:'\e0bf'}.oi-sort-descending:before{content:'\e0c0'}.oi-spreadsheet:before{content:'\e0c1'}.oi-star:before{content:'\e0c2'}.oi-sun:before{content:'\e0c3'}.oi-tablet:before{content:'\e0c4'}.oi-tag:before{content:'\e0c5'}.oi-tags:before{content:'\e0c6'}.oi-target:before{content:'\e0c7'}.oi-task:before{content:'\e0c8'}.oi-terminal:before{content:'\e0c9'}.oi-text:before{content:'\e0ca'}.oi-thumb-down:before{content:'\e0cb'}.oi-thumb-up:before{content:'\e0cc'}.oi-timer:before{content:'\e0cd'}.oi-transfer:before{content:'\e0ce'}.oi-trash:before{content:'\e0cf'}.oi-underline:before{content:'\e0d0'}.oi-vertical-align-bottom:before{content:'\e0d1'}.oi-vertical-align-center:before{content:'\e0d2'}.oi-vertical-align-top:before{content:'\e0d3'}.oi-video:before{content:'\e0d4'}.oi-volume-high:before{content:'\e0d5'}.oi-volume-low:before{content:'\e0d6'}.oi-volume-off:before{content:'\e0d7'}.oi-warning:before{content:'\e0d8'}.oi-wifi:before{content:'\e0d9'}.oi-wrench:before{content:'\e0da'}.oi-x:before{content:'\e0db'}.oi-yen:before{content:'\e0dc'}.oi-zoom-in:before{content:'\e0dd'}.oi-zoom-out:before{content:'\e0de'} -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbst16/dotnet-blazor-crud/11392b0593771e0c22d9152ec585e789e9642170/Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.eot -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbst16/dotnet-blazor-crud/11392b0593771e0c22d9152ec585e789e9642170/Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.otf -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbst16/dotnet-blazor-crud/11392b0593771e0c22d9152ec585e789e9642170/Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.ttf -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbst16/dotnet-blazor-crud/11392b0593771e0c22d9152ec585e789e9642170/Blazorcrud.Client/wwwroot/css/open-iconic/font/fonts/open-iconic.woff -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thbst16/dotnet-blazor-crud/11392b0593771e0c22d9152ec585e789e9642170/Blazorcrud.Client/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | Blazor CRUD 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Blazor CRUD

19 |

The application is loading...

20 |
21 |
22 | 23 |
24 | An unhandled error has occurred. 25 | Reload 26 | 🗙 27 |
28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Blazorcrud.Client/wwwroot/site.js: -------------------------------------------------------------------------------- 1 | function saveAsFile(filename, bytesBase64) { 2 | var link = document.createElement('a'); 3 | link.download = filename; 4 | link.href = "data:application/octet-stream;base64," + bytesBase64; 5 | document.body.appendChild(link); // Needed for Firefox 6 | link.click(); 7 | document.body.removeChild(link); 8 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Authorization/AllowAnonymouseAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Authorization; 2 | 3 | [AttributeUsage(AttributeTargets.Method)] 4 | public class AllowAnonymousAttribute : Attribute 5 | { } -------------------------------------------------------------------------------- /Blazorcrud.Server/Authorization/AuthenticateRequest.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Authorization; 2 | 3 | public class AuthenticateRequest 4 | { 5 | public string Username {get; set;} = default!; 6 | public string Password { get; set; } = default!; 7 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Authorization/AuthenticateResponse.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Shared.Models; 3 | 4 | public class AuthenticateResponse 5 | { 6 | public int Id {get; set;} 7 | public string FirstName {get; set;} = null!; 8 | public string LastName {get; set;} = null!; 9 | public string Username {get;set;} = null!; 10 | public string Token { get; set; } = default!; 11 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Authorization/AuthorizeAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Authorization; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | using Blazorcrud.Shared.Models; 6 | 7 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] 8 | public class AuthorizeAttribute : Attribute, IAuthorizationFilter 9 | { 10 | public void OnAuthorization(AuthorizationFilterContext context) 11 | { 12 | // skip authorization if action is decorated with [AllowAnonymous] attribute 13 | var allowAnonymous = context.ActionDescriptor.EndpointMetadata.OfType().Any(); 14 | if (allowAnonymous) 15 | return; 16 | 17 | // authorization 18 | var user = (User?)context.HttpContext.Items["User"]; 19 | if (user == null) 20 | context.Result = new JsonResult(new { message = "Unauthorized" }) { StatusCode = StatusCodes.Status401Unauthorized }; 21 | } 22 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Authorization/JwtMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Server.Models; 3 | 4 | public class JwtMiddleware 5 | { 6 | private readonly RequestDelegate _next; 7 | 8 | public JwtMiddleware(RequestDelegate next) 9 | { 10 | _next = next; 11 | } 12 | 13 | public async Task Invoke(HttpContext context, IUserRepository userRepository, IJwtUtils jwtUtils) 14 | { 15 | var token = context.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last(); 16 | var userId = jwtUtils.ValidateToken(token); 17 | if (userId != null) 18 | { 19 | // attach user to context on successful jwt validation 20 | context.Items["User"] = await userRepository.GetUser(userId.Value); 21 | } 22 | 23 | await _next(context); 24 | } 25 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Authorization/JwtUtils.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Authorization; 2 | using Microsoft.Extensions.Options; 3 | using Microsoft.IdentityModel.Tokens; 4 | using System.IdentityModel.Tokens.Jwt; 5 | using System.Security.Claims; 6 | using System.Text; 7 | using Blazorcrud.Shared.Models; 8 | 9 | public interface IJwtUtils 10 | { 11 | public string GenerateToken(User user); 12 | public int? ValidateToken(string token); 13 | } 14 | 15 | public class JwtUtils : IJwtUtils 16 | { 17 | private readonly AppSettings _appSettings; 18 | 19 | public JwtUtils(IOptions appSettings) 20 | { 21 | _appSettings = appSettings.Value; 22 | } 23 | 24 | public string GenerateToken(User user) 25 | { 26 | // generate token that is valid for 7 days 27 | var tokenHandler = new JwtSecurityTokenHandler(); 28 | var key = Encoding.ASCII.GetBytes(_appSettings.Secret); 29 | var tokenDescriptor = new SecurityTokenDescriptor 30 | { 31 | Subject = new ClaimsIdentity(new[] { new Claim("id", user.Id.ToString()) }), 32 | Expires = DateTime.UtcNow.AddDays(7), 33 | SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature) 34 | }; 35 | var token = tokenHandler.CreateToken(tokenDescriptor); 36 | return tokenHandler.WriteToken(token); 37 | } 38 | 39 | public int? ValidateToken(string token) 40 | { 41 | if (token == null) 42 | return null; 43 | 44 | var tokenHandler = new JwtSecurityTokenHandler(); 45 | var key = Encoding.ASCII.GetBytes(_appSettings.Secret); 46 | try 47 | { 48 | tokenHandler.ValidateToken(token, new TokenValidationParameters 49 | { 50 | ValidateIssuerSigningKey = true, 51 | IssuerSigningKey = new SymmetricSecurityKey(key), 52 | ValidateIssuer = false, 53 | ValidateAudience = false, 54 | // set clockskew to zero so tokens expire exactly at token expiration time (instead of 5 minutes later) 55 | ClockSkew = TimeSpan.Zero 56 | }, out SecurityToken validatedToken); 57 | 58 | var jwtToken = (JwtSecurityToken)validatedToken; 59 | var userId = int.Parse(jwtToken.Claims.First(x => x.Type == "id").Value); 60 | 61 | // return user id from JWT token if validation successful 62 | return userId; 63 | } 64 | catch 65 | { 66 | // return null if validation fails 67 | return null; 68 | } 69 | } 70 | } 71 | 72 | public class AppSettings 73 | { 74 | public string Secret { get; set; } = null!; 75 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Blazorcrud.Server.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | 7 | 8 | true 9 | $(NoWarn);1591 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Controllers/PersonController.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Server.Models; 3 | using Blazorcrud.Shared.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Blazorcrud.Server.Controllers 7 | { 8 | [Authorize] 9 | [ApiController] 10 | [Route("api/[controller]")] 11 | public class PersonController : ControllerBase 12 | { 13 | private readonly IPersonRepository _personRepository; 14 | 15 | public PersonController(IPersonRepository personRepository) 16 | { 17 | _personRepository = personRepository; 18 | } 19 | 20 | /// 21 | /// Returns a list of paginated people with a default page size of 5. 22 | /// 23 | [AllowAnonymous] 24 | [HttpGet] 25 | public ActionResult GetPeople([FromQuery] string? name, int page) 26 | { 27 | return Ok(_personRepository.GetPeople(name, page)); 28 | } 29 | 30 | /// 31 | /// Gets a specific person by Id. 32 | /// 33 | [AllowAnonymous] 34 | [HttpGet("{id}")] 35 | public async Task GetPerson(int id) 36 | { 37 | return Ok(await _personRepository.GetPerson(id)); 38 | } 39 | 40 | /// 41 | /// Creates a person with child addresses. 42 | /// 43 | [HttpPost] 44 | public async Task AddPerson(Person person) 45 | { 46 | return Ok(await _personRepository.AddPerson(person)); 47 | } 48 | 49 | /// 50 | /// Updates a person with a specific Id. 51 | /// 52 | [HttpPut] 53 | public async Task UpdatePerson(Person person) 54 | { 55 | return Ok(await _personRepository.UpdatePerson(person)); 56 | } 57 | 58 | /// 59 | /// Deletes a person with a specific Id. 60 | /// 61 | [HttpDelete("{id}")] 62 | public async Task DeletePerson(int id) 63 | { 64 | return Ok(await _personRepository.DeletePerson(id)); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Controllers/UploadController.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Server.Models; 3 | using Blazorcrud.Shared.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Blazorcrud.Server.Controllers 7 | { 8 | [Authorize] 9 | [ApiController] 10 | [Route("api/[controller]")] 11 | public class UploadController : ControllerBase 12 | { 13 | private readonly IUploadRepository _uploadRepository; 14 | 15 | public UploadController(IUploadRepository uploadRepository) 16 | { 17 | _uploadRepository = uploadRepository; 18 | } 19 | 20 | /// 21 | /// Returns a list of paginated uploads with a default page size of 5. 22 | /// 23 | [AllowAnonymous] 24 | [HttpGet] 25 | public ActionResult GetUploads(string? name, int page) 26 | { 27 | return Ok(_uploadRepository.GetUploads(name, page)); 28 | } 29 | 30 | /// 31 | /// Gets a specific upload by Id. 32 | /// 33 | [AllowAnonymous] 34 | [HttpGet("{id}")] 35 | public async Task GetUpload(int id) 36 | { 37 | return Ok(await _uploadRepository.GetUpload(id)); 38 | } 39 | 40 | /// 41 | /// Creates an upload with base64 encoded file 42 | /// 43 | [HttpPost] 44 | public async Task AddUpload(Upload upload) 45 | { 46 | return Ok(await _uploadRepository.AddUpload(upload)); 47 | } 48 | 49 | /// 50 | /// Updates an upload with a specific Id. 51 | /// 52 | [HttpPut] 53 | public async Task UpdateUpload(Upload upload) 54 | { 55 | return Ok(await _uploadRepository.UpdateUpload(upload)); 56 | } 57 | 58 | /// 59 | /// Deletes an upload with a specific Id. 60 | /// 61 | [HttpDelete("{id}")] 62 | public async Task DeleteUpload(int id) 63 | { 64 | return Ok(await _uploadRepository.DeleteUpload(id)); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Server.Models; 3 | using Blazorcrud.Shared.Models; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Options; 6 | 7 | namespace Blazorcrud.Server.Controllers 8 | { 9 | [Authorize] 10 | [ApiController] 11 | [Route("api/[controller]")] 12 | public class UserController : ControllerBase 13 | { 14 | private readonly IUserRepository _userRepository; 15 | private readonly AppSettings _appSettings; 16 | 17 | public UserController(IUserRepository userRepository, IOptions appSettings) 18 | { 19 | _userRepository = userRepository; 20 | _appSettings = appSettings.Value; 21 | } 22 | 23 | /// 24 | /// Authenticates a user and returns a JWT token and user details 25 | /// 26 | [AllowAnonymous] 27 | [HttpPost("authenticate")] 28 | public ActionResult Authenticate(AuthenticateRequest request) 29 | { 30 | return Ok(_userRepository.Authenticate(request)); 31 | } 32 | 33 | 34 | /// 35 | /// Returns a list of paginated users with a default page size of 5. 36 | /// 37 | [AllowAnonymous] 38 | [HttpGet] 39 | public ActionResult GetUsers([FromQuery] string? name, int page) 40 | { 41 | return Ok(_userRepository.GetUsers(name, page)); 42 | } 43 | 44 | /// 45 | /// Gets a specific user by Id. 46 | /// 47 | [AllowAnonymous] 48 | [HttpGet("{id}")] 49 | public async Task GetUser(int id) 50 | { 51 | return Ok(await _userRepository.GetUser(id)); 52 | } 53 | 54 | /// 55 | /// Creates a user and hashes password. 56 | /// 57 | [HttpPost] 58 | public async Task AddUser(User user) 59 | { 60 | return Ok(await _userRepository.AddUser(user)); 61 | } 62 | 63 | /// 64 | /// Updates a user with a specific Id, hashing password if updated. 65 | /// 66 | [HttpPut] 67 | public async Task UpdateUser(User user) 68 | { 69 | return Ok(await _userRepository.UpdateUser(user)); 70 | } 71 | 72 | /// 73 | /// Deletes a user with a specific Id. 74 | /// 75 | [HttpDelete("{id}")] 76 | public async Task DeleteUser(int id) 77 | { 78 | return Ok(await _userRepository.DeleteUser(id)); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Helpers/AppException.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Helpers; 2 | using System.Globalization; 3 | 4 | // custom exception class for throwing application specific exceptions (e.g. for validation) 5 | // that can be caught and handled within the application 6 | public class AppException : Exception 7 | { 8 | public AppException() : base() {} 9 | 10 | public AppException(string message) : base(message) { } 11 | 12 | public AppException(string message, params object[] args) 13 | : base(String.Format(CultureInfo.CurrentCulture, message, args)) 14 | { 15 | } 16 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Helpers/ErrorHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Server.Helpers; 2 | using System.Net; 3 | using System.Text.Json; 4 | 5 | public class ErrorHandlerMiddleware 6 | { 7 | private readonly RequestDelegate _next; 8 | 9 | public ErrorHandlerMiddleware(RequestDelegate next) 10 | { 11 | _next = next; 12 | } 13 | 14 | public async Task Invoke(HttpContext context) 15 | { 16 | try 17 | { 18 | await _next(context); 19 | } 20 | catch (Exception error) 21 | { 22 | var response = context.Response; 23 | response.ContentType = "application/json"; 24 | 25 | switch(error) 26 | { 27 | case AppException e: 28 | // custom application error 29 | response.StatusCode = (int)HttpStatusCode.BadRequest; 30 | break; 31 | case KeyNotFoundException e: 32 | // not found error 33 | response.StatusCode = (int)HttpStatusCode.NotFound; 34 | break; 35 | default: 36 | // unhandled error 37 | response.StatusCode = (int)HttpStatusCode.InternalServerError; 38 | break; 39 | } 40 | 41 | var result = JsonSerializer.Serialize(new { message = error?.Message }); 42 | await response.WriteAsync(result); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | 4 | namespace Blazorcrud.Server.Models 5 | { 6 | public class AppDbContext : DbContext 7 | { 8 | public AppDbContext(DbContextOptions options) : base(options) 9 | { 10 | 11 | } 12 | 13 | public DbSet People => Set(); 14 | public DbSet
Addresses => Set
(); 15 | public DbSet Uploads => Set(); 16 | public DbSet Users => Set(); 17 | } 18 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/DataGenerator.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Models; 2 | using Bogus; 3 | 4 | namespace Blazorcrud.Server.Models 5 | { 6 | public class DataGenerator 7 | { 8 | public static void Initialize(AppDbContext appDbContext) 9 | { 10 | Randomizer.Seed = new Random(32321); 11 | appDbContext.Database.EnsureDeleted(); 12 | appDbContext.Database.EnsureCreated(); 13 | if (!(appDbContext.People.Any())) 14 | { 15 | //Create test addresses 16 | var testAddresses = new Faker
() 17 | .RuleFor(a => a.Street, f => f.Address.StreetAddress()) 18 | .RuleFor(a => a.City, f => f.Address.City()) 19 | .RuleFor(a => a.State, f => f.Address.State()) 20 | .RuleFor(a => a.ZipCode, f => f.Address.ZipCode()); 21 | 22 | // Create new people 23 | var testPeople = new Faker() 24 | .RuleFor(p => p.FirstName, f => f.Name.FirstName()) 25 | .RuleFor(p => p.LastName, f => f.Name.LastName()) 26 | .RuleFor(p => p.Gender, f => f.PickRandom()) 27 | .RuleFor(p => p.PhoneNumber, f => f.Phone.PhoneNumber()) 28 | .RuleFor(p => p.Addresses, f => testAddresses.Generate(2).ToList()); 29 | 30 | var people = testPeople.Generate(25); 31 | 32 | foreach (Blazorcrud.Shared.Models.Person p in people) 33 | { 34 | appDbContext.People.Add(p); 35 | } 36 | appDbContext.SaveChanges(); 37 | } 38 | 39 | if (!(appDbContext.Uploads.Any())) 40 | { 41 | string jsonRecord = @"[{""FirstName"": ""Tim"",""LastName"": ""Bucktooth"",""Gender"": 1,""PhoneNumber"": ""717-211-3211"", 42 | ""Addresses"": [{""Street"": ""415 McKee Place"",""City"": ""Pittsburgh"",""State"": ""Pennsylvania"",""ZipCode"": ""15140"" 43 | },{ ""Street"": ""315 Gunpowder Road"",""City"": ""Mechanicsburg"",""State"": ""Pennsylvania"",""ZipCode"": ""17101"" }]}]"; 44 | var testUploads = new Faker() 45 | .RuleFor(u => u.FileName, u => u.Lorem.Word()+".json") 46 | .RuleFor(u => u.UploadTimestamp, u => u.Date.Past(1, DateTime.Now)) 47 | .RuleFor(u => u.ProcessedTimestamp, u => u.Date.Future(1, DateTime.Now)) 48 | .RuleFor(u => u.FileContent, Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(jsonRecord))); 49 | var uploads = testUploads.Generate(8); 50 | 51 | foreach (Upload u in uploads) 52 | { 53 | appDbContext.Uploads.Add(u); 54 | } 55 | appDbContext.SaveChanges(); 56 | } 57 | 58 | if (!(appDbContext.Users.Any())) 59 | { 60 | var testUsers = new Faker() 61 | .RuleFor(u => u.FirstName, u => u.Name.FirstName()) 62 | .RuleFor(u => u.LastName, u => u.Name.LastName()) 63 | .RuleFor(u => u.Username, u => u.Internet.UserName()) 64 | .RuleFor(u => u.Password, u => u.Internet.Password()); 65 | var users = testUsers.Generate(4); 66 | 67 | User customUser = new User(){ 68 | FirstName = "Terry", 69 | LastName = "Smith", 70 | Username = "admin", 71 | Password = "admin" 72 | }; 73 | 74 | users.Add(customUser); 75 | 76 | foreach (User u in users) 77 | { 78 | u.PasswordHash = BCrypt.Net.BCrypt.HashPassword(u.Password); 79 | u.Password = "**********"; 80 | appDbContext.Users.Add(u); 81 | } 82 | appDbContext.SaveChanges(); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/IPersonRepository.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Data; 2 | using Blazorcrud.Shared.Models; 3 | 4 | namespace Blazorcrud.Server.Models 5 | { 6 | public interface IPersonRepository 7 | { 8 | PagedResult GetPeople(string? name, int page); 9 | Task GetPerson(int personId); 10 | Task AddPerson(Person person); 11 | Task UpdatePerson(Person person); 12 | Task DeletePerson(int personId); 13 | } 14 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/IUploadRepository.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Data; 2 | using Blazorcrud.Shared.Models; 3 | 4 | namespace Blazorcrud.Server.Models 5 | { 6 | public interface IUploadRepository 7 | { 8 | PagedResult GetUploads(string? name, int page); 9 | Task GetUpload(int Id); 10 | Task AddUpload(Upload upload); 11 | Task UpdateUpload(Upload upload); 12 | Task DeleteUpload(int id); 13 | } 14 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Shared.Data; 3 | using Blazorcrud.Shared.Models; 4 | 5 | namespace Blazorcrud.Server.Models 6 | { 7 | public interface IUserRepository 8 | { 9 | AuthenticateResponse Authenticate(AuthenticateRequest request); 10 | PagedResult GetUsers(string? name, int page); 11 | Task GetUser(int Id); 12 | Task AddUser(User user); 13 | Task UpdateUser(User user); 14 | Task DeleteUser(int id); 15 | } 16 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/PersonRepository.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Data; 2 | using Blazorcrud.Shared.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Blazorcrud.Server.Models 6 | { 7 | public class PersonRepository:IPersonRepository 8 | { 9 | private readonly AppDbContext _appDbContext; 10 | 11 | public PersonRepository(AppDbContext appDbContext) 12 | { 13 | _appDbContext = appDbContext; 14 | } 15 | 16 | public async Task AddPerson(Person person) 17 | { 18 | var result = await _appDbContext.People.AddAsync(person); 19 | await _appDbContext.SaveChangesAsync(); 20 | return result.Entity; 21 | } 22 | 23 | public async Task DeletePerson(int personId) 24 | { 25 | var result = await _appDbContext.People.FirstOrDefaultAsync(p => p.PersonId==personId); 26 | if (result!=null) 27 | { 28 | _appDbContext.People.Remove(result); 29 | await _appDbContext.SaveChangesAsync(); 30 | } 31 | else 32 | { 33 | throw new KeyNotFoundException("Person not found"); 34 | } 35 | return result; 36 | } 37 | 38 | public async Task GetPerson(int personId) 39 | { 40 | var result = await _appDbContext.People 41 | .Include(p => p.Addresses) 42 | .FirstOrDefaultAsync(p => p.PersonId==personId); 43 | if (result != null) 44 | { 45 | return result; 46 | } 47 | else 48 | { 49 | throw new KeyNotFoundException("Person not found"); 50 | } 51 | } 52 | 53 | public PagedResult GetPeople(string? name, int page) 54 | { 55 | int pageSize = 5; 56 | 57 | if (name != null) 58 | { 59 | return _appDbContext.People 60 | .Where(p => p.FirstName.Contains(name, StringComparison.CurrentCultureIgnoreCase) || 61 | p.LastName.Contains(name, StringComparison.CurrentCultureIgnoreCase)) 62 | .OrderBy(p => p.PersonId) 63 | .Include(p => p.Addresses) 64 | .GetPaged(page, pageSize); 65 | } 66 | else 67 | { 68 | return _appDbContext.People 69 | .OrderBy(p => p.PersonId) 70 | .Include(p => p.Addresses) 71 | .GetPaged(page, pageSize); 72 | } 73 | } 74 | 75 | public async Task UpdatePerson(Person person) 76 | { 77 | var result = await _appDbContext.People.Include("Addresses").FirstOrDefaultAsync(p => p.PersonId==person.PersonId); 78 | if (result!=null) 79 | { 80 | // Update existing person 81 | _appDbContext.Entry(result).CurrentValues.SetValues(person); 82 | 83 | // Remove deleted addresses 84 | foreach (var existingAddress in result.Addresses.ToList()) 85 | { 86 | if(!person.Addresses.Any(o => o.AddressId == existingAddress.AddressId)) 87 | _appDbContext.Addresses.Remove(existingAddress); 88 | } 89 | 90 | // Update and Insert Addresses 91 | foreach (var addressModel in person.Addresses) 92 | { 93 | var existingAddress = result.Addresses 94 | .Where(a => a.AddressId == addressModel.AddressId) 95 | .SingleOrDefault(); 96 | if (existingAddress != null) 97 | _appDbContext.Entry(existingAddress).CurrentValues.SetValues(addressModel); 98 | else 99 | { 100 | var newAddress = new Address 101 | { 102 | AddressId = addressModel.AddressId, 103 | Street = addressModel.Street, 104 | City = addressModel.City, 105 | State = addressModel.State, 106 | ZipCode = addressModel.ZipCode 107 | }; 108 | result.Addresses.Add(newAddress); 109 | } 110 | } 111 | await _appDbContext.SaveChangesAsync(); 112 | } 113 | else 114 | { 115 | throw new KeyNotFoundException("Person not found"); 116 | } 117 | return result; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/UploadRepository.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Shared.Data; 2 | using Blazorcrud.Shared.Models; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace Blazorcrud.Server.Models 6 | { 7 | public class UploadRepository : IUploadRepository 8 | { 9 | private readonly AppDbContext _appDbContext; 10 | 11 | public UploadRepository(AppDbContext appDbContext) 12 | { 13 | _appDbContext = appDbContext; 14 | } 15 | 16 | public async Task AddUpload(Upload upload) 17 | { 18 | var result = await _appDbContext.Uploads.AddAsync(upload); 19 | await _appDbContext.SaveChangesAsync(); 20 | return result.Entity; 21 | } 22 | 23 | public async Task DeleteUpload(int Id) 24 | { 25 | var result = await _appDbContext.Uploads.FirstOrDefaultAsync(u => u.Id==Id); 26 | if (result!=null) 27 | { 28 | _appDbContext.Uploads.Remove(result); 29 | await _appDbContext.SaveChangesAsync(); 30 | } 31 | else 32 | { 33 | throw new KeyNotFoundException("Upload not found"); 34 | } 35 | return result; 36 | } 37 | 38 | public async Task GetUpload(int Id) 39 | { 40 | var result = await _appDbContext.Uploads.FirstOrDefaultAsync(u => u.Id==Id); 41 | if(result != null) 42 | { 43 | return result; 44 | } 45 | else 46 | { 47 | throw new KeyNotFoundException("Upload not found"); 48 | } 49 | } 50 | 51 | public PagedResult GetUploads(string? name, int page) 52 | { 53 | int pageSize = 5; 54 | 55 | if (name != null) 56 | { 57 | return _appDbContext.Uploads 58 | .Where(u => u.FileName.Contains(name, StringComparison.CurrentCultureIgnoreCase)) 59 | .OrderBy(u => u.UploadTimestamp) 60 | .GetPaged(page, pageSize); 61 | } 62 | else 63 | { 64 | return _appDbContext.Uploads 65 | .OrderBy(u => u.UploadTimestamp) 66 | .GetPaged(page, pageSize); 67 | } 68 | } 69 | 70 | public async Task UpdateUpload(Upload upload) 71 | { 72 | var result = await _appDbContext.Uploads.FirstOrDefaultAsync(u => u.Id==upload.Id); 73 | if (result!=null) 74 | { 75 | // Update existing upload 76 | _appDbContext.Entry(result).CurrentValues.SetValues(upload); 77 | await _appDbContext.SaveChangesAsync(); 78 | } 79 | else 80 | { 81 | throw new KeyNotFoundException("Upload not found"); 82 | } 83 | return result; 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Models/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Server.Helpers; 3 | using Blazorcrud.Shared.Data; 4 | using Blazorcrud.Shared.Models; 5 | using Microsoft.EntityFrameworkCore; 6 | 7 | namespace Blazorcrud.Server.Models 8 | { 9 | public class UserRepository : IUserRepository 10 | { 11 | private readonly AppDbContext _appDbContext; 12 | private IJwtUtils _jwtUtils; 13 | 14 | public UserRepository(AppDbContext appDbContext, IJwtUtils jwtUtils) 15 | { 16 | _appDbContext = appDbContext; 17 | _jwtUtils = jwtUtils; 18 | } 19 | 20 | public AuthenticateResponse Authenticate(AuthenticateRequest request) 21 | { 22 | var _user = _appDbContext.Users.SingleOrDefault(u => u.Username == request.Username); 23 | 24 | // validate 25 | if (_user == null || !BCrypt.Net.BCrypt.Verify(request.Password, _user.PasswordHash)) 26 | throw new AppException("Username or password is incorrect"); 27 | 28 | // authentication successful 29 | AuthenticateResponse response = new AuthenticateResponse(); 30 | response.Id = _user.Id; 31 | response.LastName = _user.LastName; 32 | response.FirstName = _user.FirstName; 33 | response.Username = _user.Username; 34 | response.Token = _jwtUtils.GenerateToken(_user); 35 | return response; 36 | } 37 | 38 | public PagedResult GetUsers(string? name, int page) 39 | { 40 | int pageSize = 5; 41 | 42 | if (name != null) 43 | { 44 | return _appDbContext.Users 45 | .Where(u => u.FirstName.Contains(name, StringComparison.CurrentCultureIgnoreCase) || 46 | u.LastName.Contains(name, StringComparison.CurrentCultureIgnoreCase) || 47 | u.Username.Contains(name, StringComparison.CurrentCultureIgnoreCase)) 48 | .OrderBy(u => u.Username) 49 | .GetPaged(page, pageSize); 50 | } 51 | else 52 | { 53 | return _appDbContext.Users 54 | .OrderBy(u => u.Username) 55 | .GetPaged(page, pageSize); 56 | } 57 | } 58 | 59 | public async Task GetUser(int Id) 60 | { 61 | var result = await _appDbContext.Users.FirstOrDefaultAsync(u => u.Id==Id); 62 | if (result != null) 63 | { 64 | return result; 65 | } 66 | else 67 | { 68 | throw new KeyNotFoundException("User not found"); 69 | } 70 | } 71 | 72 | public async Task AddUser(User user) 73 | { 74 | // validate unique 75 | if (_appDbContext.Users.Any(u => u.Username == user.Username)) 76 | throw new AppException("Username '" + user.Username + "' is already taken"); 77 | 78 | // hash password 79 | user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(user.Password); 80 | Console.WriteLine(user.Password + " ==> " + user.PasswordHash); 81 | user.Password = "**********"; 82 | 83 | var result = await _appDbContext.Users.AddAsync(user); 84 | await _appDbContext.SaveChangesAsync(); 85 | return result.Entity; 86 | } 87 | 88 | public async Task UpdateUser(User user) 89 | { 90 | var result = await _appDbContext.Users.FirstOrDefaultAsync(u => u.Id==user.Id); 91 | 92 | // cannot update admin 93 | if (result == null || result.Username == "admin") 94 | throw new AppException("Admin may not be updated"); 95 | 96 | // validate unique 97 | if (user.Username != result.Username && _appDbContext.Users.Any(u => u.Username == user.Username)) 98 | throw new AppException("Username '" + user.Username + "' is already taken"); 99 | 100 | // hash password if entered 101 | if(!string.IsNullOrEmpty(user.Password) && user.Password != result.Password) 102 | { 103 | user.PasswordHash = BCrypt.Net.BCrypt.HashPassword(user.Password); 104 | user.Password = "**********"; 105 | } 106 | 107 | if (result!=null) 108 | { 109 | // Update existing user 110 | _appDbContext.Entry(result).CurrentValues.SetValues(user); 111 | await _appDbContext.SaveChangesAsync(); 112 | } 113 | else 114 | { 115 | throw new KeyNotFoundException("User not found"); 116 | } 117 | return result; 118 | } 119 | 120 | public async Task DeleteUser(int Id) 121 | { 122 | var result = await _appDbContext.Users.FirstOrDefaultAsync(u => u.Id==Id); 123 | 124 | // cannot delete admin 125 | if (result == null || result.Username == "admin") 126 | throw new AppException("Admin may not be deleted"); 127 | 128 | if (result!=null) 129 | { 130 | _appDbContext.Users.Remove(result); 131 | await _appDbContext.SaveChangesAsync(); 132 | } 133 | else 134 | { 135 | throw new KeyNotFoundException("User not found"); 136 | } 137 | return result; 138 | } 139 | } 140 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model Blazorcrud.Server.Pages.ErrorModel 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Error 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Error.

19 |

An error occurred while processing your request.

20 | 21 | @if (Model.ShowRequestId) 22 | { 23 |

24 | Request ID: @Model.RequestId 25 |

26 | } 27 | 28 |

Development Mode

29 |

30 | Swapping to the Development environment displays detailed information about the error that occurred. 31 |

32 |

33 | The Development environment shouldn't be enabled for deployed applications. 34 | It can result in displaying sensitive information from exceptions to end users. 35 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 36 | and restarting the app. 37 |

38 |
39 |
40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.RazorPages; 4 | 5 | namespace Blazorcrud.Server.Pages; 6 | 7 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 8 | [IgnoreAntiforgeryToken] 9 | public class ErrorModel : PageModel 10 | { 11 | public string? RequestId { get; set; } 12 | 13 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 14 | 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public void OnGet() 23 | { 24 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Program.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Authorization; 2 | using Blazorcrud.Server.Helpers; 3 | using Blazorcrud.Server.Models; 4 | using Blazorcrud.Server.Services; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.OpenApi.Models; 7 | using System.Reflection; 8 | using Quartz; 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | // Add services to the container. 13 | builder.Services.AddControllersWithViews(); 14 | builder.Services.AddRazorPages(); 15 | builder.Services.AddDbContext(opt => opt.UseInMemoryDatabase("BlazorServerCRUD")); 16 | builder.Services.AddScoped(); 17 | builder.Services.AddScoped(); 18 | builder.Services.AddScoped(); 19 | builder.Services.AddScoped(); 20 | 21 | builder.Services.AddSwaggerGen(c => 22 | { 23 | c.SwaggerDoc("v1", new OpenApiInfo 24 | { 25 | Title = "Blazor CRUD API", 26 | Version = "v1", 27 | Description = "CRUD API Services that act as the backend to the Blazor CRUD website." 28 | }); 29 | 30 | // Set the comments path for the Swagger JSON and UI. 31 | var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; 32 | var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); 33 | c.IncludeXmlComments(xmlPath); 34 | c.CustomSchemaIds(r => r.FullName); 35 | }); 36 | 37 | builder.Services.AddQuartz(q => 38 | { 39 | q.AddJobAndTrigger(builder.Configuration); 40 | }); 41 | builder.Services.AddQuartzHostedService( 42 | q => q.WaitForJobsToComplete = true); 43 | 44 | builder.Services.Configure(builder.Configuration.GetSection("AppSettings")); 45 | 46 | var app = builder.Build(); 47 | 48 | using (var scope = app.Services.CreateScope()) 49 | { 50 | var services = scope.ServiceProvider; 51 | 52 | try 53 | { 54 | var appDbContext = services.GetRequiredService(); 55 | DataGenerator.Initialize(appDbContext); 56 | } 57 | catch (Exception ex) 58 | { 59 | var logger = services.GetRequiredService>(); 60 | logger.LogError(ex, "An error occurred creating the DB."); 61 | } 62 | } 63 | 64 | // Configure the HTTP request pipeline. 65 | if (app.Environment.IsDevelopment()) 66 | { 67 | app.UseWebAssemblyDebugging(); 68 | } 69 | else 70 | { 71 | app.UseExceptionHandler("/Error"); 72 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 73 | app.UseHsts(); 74 | } 75 | 76 | app.UseSwagger(); 77 | app.UseSwaggerUI(c => 78 | { 79 | c.SwaggerEndpoint("/swagger/v1/swagger.json", "blazorcrud.api v1"); 80 | c.DefaultModelsExpandDepth(-1); 81 | }); 82 | 83 | app.UseHttpsRedirection(); 84 | app.UseBlazorFrameworkFiles(); 85 | app.UseStaticFiles(); 86 | app.UseRouting(); 87 | 88 | app.UseMiddleware(); 89 | app.UseMiddleware(); 90 | 91 | app.MapRazorPages(); 92 | app.MapControllers(); 93 | app.MapFallbackToFile("index.html"); 94 | 95 | app.Run(); -------------------------------------------------------------------------------- /Blazorcrud.Server/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:17056", 7 | "sslPort": 44331 8 | } 9 | }, 10 | "profiles": { 11 | "Blazorcrud.Server": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 16 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 25 | "environmentVariables": { 26 | "ASPNETCORE_ENVIRONMENT": "Development" 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Blazorcrud.Server/Services/ServiceCollectionQuartzConfigurationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Quartz; 2 | 3 | namespace Blazorcrud.Server.Services 4 | { 5 | public static class ServiceCollectionQuartzConfiguratorExtensions 6 | { 7 | public static void AddJobAndTrigger( 8 | this IServiceCollectionQuartzConfigurator quartz, 9 | IConfiguration config) 10 | where T : IJob 11 | { 12 | string jobName = typeof(T).Name; 13 | 14 | var configKey = $"Quartz:{jobName}"; 15 | var cronSchedule = config[configKey]; 16 | 17 | if (string.IsNullOrEmpty(cronSchedule)) 18 | { 19 | throw new Exception($"No Quartz.NET Cron schedule found for job in configuration at {configKey}"); 20 | } 21 | 22 | 23 | var jobKey = new JobKey(jobName); 24 | quartz.AddJob(opts => opts.WithIdentity(jobKey)); 25 | 26 | quartz.AddTrigger(opts => opts 27 | .ForJob(jobKey) 28 | .WithIdentity(jobName + "-trigger") 29 | .WithCronSchedule(cronSchedule)); 30 | 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/Services/UploadProcessorJob.cs: -------------------------------------------------------------------------------- 1 | using Blazorcrud.Server.Models; 2 | using Blazorcrud.Shared.Models; 3 | using Quartz; 4 | using System.Text.Json; 5 | 6 | namespace Blazorcrud.Server.Services 7 | { 8 | [DisallowConcurrentExecution] 9 | public class UploadProcessorJob : IJob 10 | { 11 | private readonly ILogger _logger; 12 | private readonly AppDbContext _appDbContext; 13 | 14 | public UploadProcessorJob(ILogger logger, AppDbContext appDbContext) 15 | { 16 | _logger = logger; 17 | _appDbContext = appDbContext; 18 | } 19 | 20 | public Task Execute(IJobExecutionContext context) 21 | { 22 | _logger.LogInformation("File Processing Job Initiated: " + DateTime.Now.ToString("dddd, MMMM dd, yyyy HH:mm:ss.fffK")); 23 | List unprocessedUploads = GetUnprocessedUploads(); 24 | _logger.LogInformation("Count of files requiring processing: " + unprocessedUploads.Count); 25 | if (unprocessedUploads.Count > 0) 26 | { 27 | ProcessFiles(unprocessedUploads); 28 | } 29 | _logger.LogInformation("File Processing Job Completed: " + DateTime.Now.ToString("dddd, MMMM dd, yyyy HH:mm:ss.fffK")); 30 | return Task.CompletedTask; 31 | } 32 | 33 | private List GetUnprocessedUploads() 34 | { 35 | List unprocessedUploads = (from u in _appDbContext.Uploads 36 | where u.ProcessedTimestamp.HasValue != true 37 | select u).ToList(); 38 | return unprocessedUploads; 39 | } 40 | 41 | private void ProcessFiles(List uploads) 42 | { 43 | foreach (Upload u in uploads) 44 | { 45 | byte[] base64Data = System.Convert.FromBase64String(u.FileContent); 46 | string base64Decoded = System.Text.ASCIIEncoding.UTF8.GetString(base64Data); 47 | 48 | try 49 | { 50 | List people = JsonSerializer.Deserialize>(base64Decoded) ?? new List();; 51 | foreach (Person p in people) 52 | { 53 | _appDbContext.People.Add(p); 54 | } 55 | _appDbContext.SaveChanges(); 56 | 57 | // Update file processing status in the system 58 | u.ProcessedTimestamp = DateTime.Now; 59 | _appDbContext.SaveChanges(); 60 | _logger.LogInformation("File Id: " + u.Id + " Name: " + u.FileName + " processed at " + DateTime.Now.ToString("dddd, MMMM dd, yyyy HH:mm:ss.fffK")); 61 | } 62 | catch (Exception ex) 63 | { 64 | _logger.LogInformation("Exception Type: " + ex.GetType()); 65 | } 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /Blazorcrud.Server/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Blazorcrud.Server/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "Quartz": { 9 | "UploadProcessorJob": "0/30 * * * * ?" 10 | }, 11 | "AppSettings": { 12 | "Secret": "uVYlJzFQx6Zc+TpOizv/xQkt9DJG8UMNoowKzQcMadA=" 13 | }, 14 | "AllowedHosts": "*" 15 | } 16 | -------------------------------------------------------------------------------- /Blazorcrud.Server/test.http: -------------------------------------------------------------------------------- 1 | @host = https://localhost:5001/api 2 | @contentType = application/json 3 | 4 | GET {{host}}/person?name=wilford HTTP/1.1 5 | Content-Type: {{contentType}} 6 | 7 | ### 8 | 9 | GET {{host}}/person?page=3 HTTP/1.1 10 | Content-Type: {{contentType}} 11 | 12 | ### 13 | 14 | GET {{host}}/person/1 HTTP/1.1 15 | Content-Type: {{contentType}} 16 | 17 | ### 18 | 19 | POST {{host}}/person HTTP/1.1 20 | Content-Type: {{contentType}} 21 | 22 | { 23 | "firstName": "Tim", 24 | "lastName": "Bucktooth", 25 | "gender": 1, 26 | "phoneNumber": "717-211-3211", 27 | "addresses": [ 28 | { 29 | "street": "415 McKee Place", 30 | "city": "Pittsburgh", 31 | "state": "Pennsylvania", 32 | "zipCode": "15140" 33 | }, 34 | { 35 | "street": "315 Gunpowder Road", 36 | "city": "Mechanicsburg", 37 | "state": "Pennsylvania", 38 | "zipCode": "17101" 39 | } 40 | ] 41 | } 42 | 43 | ### 44 | 45 | PUT {{host}}/person HTTP/1.1 46 | Content-Type: {{contentType}} 47 | 48 | { 49 | "personId": 1, 50 | "firstName": "Alfred", 51 | "lastName": "Newman", 52 | "gender": 1, 53 | "phoneNumber": "631-732-1900", 54 | "addresses": [ 55 | { 56 | "addressId": 1, 57 | "street": "13342 Zackery Plains", 58 | "city": "Selden", 59 | "state": "New York", 60 | "zipCode": "11784" 61 | }, 62 | { 63 | "addressId": 2, 64 | "street": "9224 Bell Isle", 65 | "city": "Coram", 66 | "state": "New York", 67 | "zipCode": "17201" 68 | } 69 | ] 70 | } 71 | 72 | ### 73 | 74 | DELETE {{host}}/person/13 HTTP/1.1 75 | Content-Type: {{contentType}} 76 | 77 | ### 78 | 79 | GET {{host}}/upload?name=sample HTTP/1.1 80 | Content-Type: {{contentType}} 81 | 82 | ### 83 | 84 | GET {{host}}/upload?page=2 HTTP/1.1 85 | Content-Type: {{contentType}} 86 | 87 | ### 88 | 89 | GET {{host}}/upload/1 HTTP/1.1 90 | Content-Type: {{contentType}} 91 | 92 | ### 93 | 94 | GET {{host}}/user?page=1 HTTP/1.1 95 | Content-Type: {{contentType}} 96 | 97 | ### 98 | 99 | GET {{host}}/user/1 HTTP/1.1 100 | Content-Type: {{contentType}} 101 | 102 | ### 103 | 104 | POST {{host}}/user/authenticate HTTP/1.1 105 | Content-Type: {{contentType}} 106 | 107 | { 108 | "username": "admin", 109 | "password": "admin" 110 | } 111 | 112 | ### 113 | 114 | POST {{host}}/user HTTP/1.1 115 | Content-Type: {{contentType}} 116 | Authorization: Bearer xxxx 117 | 118 | { 119 | "firstName": "Timothy", 120 | "lastName": "Hackorama", 121 | "username": "thackaraama", 122 | "password": "password123" 123 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Blazorcrud.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net8.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Blazorcrud.Shared/Data/PagedResultBase.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Data 2 | { 3 | public abstract class PagedResultBase 4 | { 5 | public int CurrentPage { get; set; } 6 | public int PageCount { get; set; } 7 | public int PageSize { get; set; } 8 | public int RowCount { get; set; } 9 | 10 | public int FirstRowOnPage 11 | { 12 | get { return (CurrentPage - 1) * PageSize + 1; } 13 | } 14 | 15 | public int LastRowOnPage 16 | { 17 | get { return Math.Min(CurrentPage * PageSize, RowCount); } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Data/PagedResultExtension.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Data 2 | { 3 | public static class PagedResultExtensions 4 | { 5 | public static PagedResult GetPaged(this IQueryable query, int page, int pageSize) where T : class 6 | { 7 | var result = new PagedResult(); 8 | result.CurrentPage = page; 9 | result.PageSize = pageSize; 10 | result.RowCount = query.Count(); 11 | 12 | var pageCount = (double)result.RowCount / pageSize; 13 | result.PageCount = (int)Math.Ceiling(pageCount); 14 | 15 | var skip = (page - 1) * pageSize; 16 | result.Results = query.Skip(skip).Take(pageSize).ToList(); 17 | 18 | return result; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Data/PagedResultT.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Data 2 | { 3 | public class PagedResult : PagedResultBase where T : class 4 | { 5 | public IList Results { get; set; } 6 | 7 | public PagedResult() 8 | { 9 | Results = new List(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Models 2 | { 3 | public class Address 4 | { 5 | public int AddressId { get; set; } 6 | public string Street { get; set; } = default!; 7 | public string City {get; set;} = default!; 8 | public string State {get; set;} = default!; 9 | public string ZipCode {get; set;} = default!; 10 | } 11 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/AddressValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Blazorcrud.Shared.Models 4 | { 5 | public class AddressValidator : AbstractValidator
6 | { 7 | public AddressValidator() 8 | { 9 | RuleLevelCascadeMode = CascadeMode.Stop; 10 | 11 | RuleFor(address => address.Street).NotEmpty().WithMessage("Street is a required field.") 12 | .Length(5, 50).WithMessage("Street must be between 5 and 50 characters."); 13 | RuleFor(address => address.City).NotEmpty().WithMessage("City is a required field.") 14 | .Length(5, 50).WithMessage("City must be between 5 and 50 characters."); 15 | RuleFor(address => address.State).NotEmpty().WithMessage("State is a required field.") 16 | .Length(5, 50).WithMessage("State must be between 5 and 50 characters."); 17 | RuleFor(address => address.ZipCode).NotEmpty().WithMessage("Zip code is a required field.") 18 | .Length(5, 30).WithMessage("Zip code must be between 5 and 30 characters."); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/Gender.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Models 2 | { 3 | public enum Gender 4 | { 5 | Male, 6 | Female, 7 | Other 8 | } 9 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/Person.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Models 2 | { 3 | public class Person 4 | { 5 | public int PersonId { get; set; } 6 | public string FirstName { get; set; } = default!; 7 | public string LastName {get; set;} = default!; 8 | public Gender Gender {get; set;} 9 | public string PhoneNumber {get; set;} = default!; 10 | public bool IsDeleting {get; set;} = default!; 11 | public List
Addresses {get; set;} = default!; 12 | } 13 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/PersonValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Blazorcrud.Shared.Models 4 | { 5 | public class PersonValidator : AbstractValidator 6 | { 7 | public PersonValidator() 8 | { 9 | RuleLevelCascadeMode = CascadeMode.Stop; 10 | 11 | RuleFor(person => person.FirstName).NotEmpty().WithMessage("First name is a required field.") 12 | .Length(3, 50).WithMessage("First name must be between 3 and 50 characters."); 13 | RuleFor(person => person.LastName).NotEmpty().WithMessage("Last name is a required field.") 14 | .Length(3, 50).WithMessage("Last name must be between 3 and 50 characters."); 15 | RuleFor(person => person.Gender).IsInEnum() 16 | .WithMessage("Gender is a required field."); 17 | RuleFor(person => person.PhoneNumber).NotEmpty().WithMessage("Phone number is a required field.") 18 | .Length(5, 50).WithMessage("Phone number must be between 5 and 50 characters."); 19 | RuleFor(person => person.Addresses).NotEmpty().WithMessage("You have to define at least one address per person"); 20 | RuleForEach(person => person.Addresses).SetValidator(new AddressValidator()); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/Upload.cs: -------------------------------------------------------------------------------- 1 | namespace Blazorcrud.Shared.Models 2 | { 3 | public class Upload 4 | { 5 | public int Id { get; set; } 6 | public string FileName { get; set; } = default!; 7 | public DateTime UploadTimestamp {get; set;} = default!; 8 | public DateTime? ProcessedTimestamp {get; set;} = default!; 9 | public string FileContent {get; set;} = default!; 10 | } 11 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/UploadValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Blazorcrud.Shared.Models 4 | { 5 | public class UploadValidator : AbstractValidator 6 | { 7 | public UploadValidator() 8 | { 9 | RuleLevelCascadeMode = CascadeMode.Stop; 10 | 11 | RuleFor(upload => upload.FileName).NotEmpty().WithMessage("File name is a required field.") 12 | .Length(5, 50).WithMessage("File name must be between 5 and 50 characters."); 13 | RuleFor(upload => upload.FileContent).NotEmpty().WithMessage("Uploaded file is required."); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/User.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Blazorcrud.Shared.Models 4 | { 5 | public class User 6 | { 7 | public int Id {get; set;} 8 | public string FirstName {get; set;} = default!; 9 | public string LastName {get; set;} = default!; 10 | public string Username {get; set;} = default!; 11 | public string Password {get; set;} = default!; 12 | public string? Token {get; set;} = default!; 13 | public bool IsDeleting {get; set;} = default!; 14 | [JsonIgnore] 15 | public string? PasswordHash {get; set;} 16 | } 17 | } -------------------------------------------------------------------------------- /Blazorcrud.Shared/Models/UserValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace Blazorcrud.Shared.Models 4 | { 5 | public class UserValidator : AbstractValidator 6 | { 7 | public UserValidator() 8 | { 9 | RuleLevelCascadeMode = CascadeMode.Stop; 10 | 11 | RuleFor(user => user.FirstName).NotEmpty().WithMessage("First name is a required field.") 12 | .Length(3, 50).WithMessage("First name must be between 3 and 50 characters."); 13 | RuleFor(user => user.LastName).NotEmpty().WithMessage("Last name is a required field.") 14 | .Length(3, 50).WithMessage("Last name must be between 3 and 50 characters."); 15 | RuleFor(user => user.Username).NotEmpty().WithMessage("User name is a required field.") 16 | .Length(3, 50).WithMessage("User name must be between 3 and 50 characters."); 17 | RuleFor(user => user.Password).NotEmpty().WithMessage("Password is a required field.") 18 | .Length(6, 50).WithMessage("Password must be between 6 and 50 characters."); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /Blazorcrud.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorcrud.Shared", "Blazorcrud.Shared\Blazorcrud.Shared.csproj", "{A7421497-C804-4BC0-A73B-430210F702F9}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorcrud.Client", "Blazorcrud.Client\Blazorcrud.Client.csproj", "{070D7A79-4D82-43B2-9E2A-21D00B3034B2}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Blazorcrud.Server", "Blazorcrud.Server\Blazorcrud.Server.csproj", "{75348867-CC83-4724-8011-5D5CB07010BE}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {A7421497-C804-4BC0-A73B-430210F702F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {A7421497-C804-4BC0-A73B-430210F702F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {A7421497-C804-4BC0-A73B-430210F702F9}.Debug|x64.ActiveCfg = Debug|Any CPU 28 | {A7421497-C804-4BC0-A73B-430210F702F9}.Debug|x64.Build.0 = Debug|Any CPU 29 | {A7421497-C804-4BC0-A73B-430210F702F9}.Debug|x86.ActiveCfg = Debug|Any CPU 30 | {A7421497-C804-4BC0-A73B-430210F702F9}.Debug|x86.Build.0 = Debug|Any CPU 31 | {A7421497-C804-4BC0-A73B-430210F702F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {A7421497-C804-4BC0-A73B-430210F702F9}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {A7421497-C804-4BC0-A73B-430210F702F9}.Release|x64.ActiveCfg = Release|Any CPU 34 | {A7421497-C804-4BC0-A73B-430210F702F9}.Release|x64.Build.0 = Release|Any CPU 35 | {A7421497-C804-4BC0-A73B-430210F702F9}.Release|x86.ActiveCfg = Release|Any CPU 36 | {A7421497-C804-4BC0-A73B-430210F702F9}.Release|x86.Build.0 = Release|Any CPU 37 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Debug|x64.ActiveCfg = Debug|Any CPU 40 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Debug|x64.Build.0 = Debug|Any CPU 41 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Debug|x86.ActiveCfg = Debug|Any CPU 42 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Debug|x86.Build.0 = Debug|Any CPU 43 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Release|x64.ActiveCfg = Release|Any CPU 46 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Release|x64.Build.0 = Release|Any CPU 47 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Release|x86.ActiveCfg = Release|Any CPU 48 | {070D7A79-4D82-43B2-9E2A-21D00B3034B2}.Release|x86.Build.0 = Release|Any CPU 49 | {75348867-CC83-4724-8011-5D5CB07010BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {75348867-CC83-4724-8011-5D5CB07010BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {75348867-CC83-4724-8011-5D5CB07010BE}.Debug|x64.ActiveCfg = Debug|Any CPU 52 | {75348867-CC83-4724-8011-5D5CB07010BE}.Debug|x64.Build.0 = Debug|Any CPU 53 | {75348867-CC83-4724-8011-5D5CB07010BE}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {75348867-CC83-4724-8011-5D5CB07010BE}.Debug|x86.Build.0 = Debug|Any CPU 55 | {75348867-CC83-4724-8011-5D5CB07010BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {75348867-CC83-4724-8011-5D5CB07010BE}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {75348867-CC83-4724-8011-5D5CB07010BE}.Release|x64.ActiveCfg = Release|Any CPU 58 | {75348867-CC83-4724-8011-5D5CB07010BE}.Release|x64.Build.0 = Release|Any CPU 59 | {75348867-CC83-4724-8011-5D5CB07010BE}.Release|x86.ActiveCfg = Release|Any CPU 60 | {75348867-CC83-4724-8011-5D5CB07010BE}.Release|x86.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | EndGlobal 63 | -------------------------------------------------------------------------------- /Create-Env.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | group="beckshome-container-apps-rg" 4 | location="eastus" 5 | env="dotnet-blazor-env" 6 | app="dotnet-blazor-crud" 7 | image="thbst16/dotnet-blazor-crud:latest" 8 | 9 | # az group create --name $group --location $location 10 | # echo 11 | # az containerapp env create -n $env -g $group --location $location 12 | # echo 13 | az containerapp create -n $app -g $group --image $image --environment $env --min-replicas 0 14 | echo 15 | az containerapp ingress enable -n $app -g $group --type external --target-port 0 --transport auto -------------------------------------------------------------------------------- /Delete-env.bash: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | group="beckshome-container-apps-rg" 4 | app="dotnet-blazor-crud" 5 | 6 | az containerapp delete -g $group -n $app 7 | # echo 8 | # az group delete --name $group -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ### PREPARE 2 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env 3 | WORKDIR /src 4 | 5 | ### Copy csproj and sln and restore as distinct layers 6 | COPY Blazorcrud.Client/*.csproj Blazorcrud.Client/ 7 | COPY Blazorcrud.Server/*.csproj Blazorcrud.Server/ 8 | COPY Blazorcrud.Shared/*.csproj Blazorcrud.Shared/ 9 | COPY Blazorcrud.sln . 10 | RUN dotnet restore 11 | 12 | ### PUBLISH 13 | FROM build-env AS publish-env 14 | COPY . . 15 | RUN dotnet publish "Blazorcrud.sln" -c Release -o /app 16 | 17 | ### RUNTIME IMAGE 18 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime-env 19 | WORKDIR /app 20 | COPY --from=publish-env /app . 21 | ENV ASPNETCORE_URLS=http://+:80 22 | EXPOSE 80 23 | ENTRYPOINT ["dotnet", "Blazorcrud.Server.dll"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dotnet-blazor-crud 2 | [![Build Status](https://beckshome.visualstudio.com/dotnet-blazor-crud/_apis/build/status/thbst16.dotnet-blazor-crud?branchName=master)](https://beckshome.visualstudio.com/dotnet-blazor-crud/_build/latest?definitionId=10&branchName=master) 3 | ![Docker Image Version (latest by date)](https://img.shields.io/docker/v/thbst16/dotnet-blazor-crud?logo=docker) 4 | 5 | Blazor CRUD is a demo application built with the [Blazor](https://blazor.net) framework using the client-side hosting model with WebAssembly in the browser invoking .NET Core REST APIs secured by a JWT service. To browse the two components of the application, follow the links below. For authenticated pages and APIs, use the credentials (admin / admin). 6 | * [Blazor CRUD Application](https://dotnet-blazor-crud.wittycoast-850643a6.eastus.azurecontainerapps.io) - A client side hosted WASM application built using Blazor. The application highlights CRUD data entry for entities, data pagination, client-side validation using Data Annotations, and authentication and authorization using JWT tokens. 7 | * [Blazor CRUD REST API](https://dotnet-blazor-crud.wittycoast-850643a6.eastus.azurecontainerapps.io/swagger/index.html) - A REST API for CRUD with non-read API calls secured with JWT. The API includes a call to authenticate users and receive a JWT bearer token. 8 | 9 | Blazor CRUD uses the following DevOps environment and tools to support a CI / CD process: 10 | * [GitHub Source Code Repository](https://github.com/thbst16/dotnet-blazor-crud) - All source code is stored in the GitHub repository, which is where you currently find yourself. 11 | * [Azure DevOps for CI/CD](https://beckshome.visualstudio.com/dotnet-blazor-crud/_build) - Azure DevOps is used for continunous integration and continuous delivery (CI/CD). Builds and deployments are initiated with every cheackin to the main brach of the solution in GitHub. 12 | * [DockerHub for Container Storage](https://hub.docker.com/repository/docker/thbst16/dotnet-blazor-crud/general) - DockerHub is where BlazorCrud tagged Docker images are stored after being created in the CI/CD pipeline. The can also be pulled and run locally by using the command "docker pull thbst16/blazor-crud". 13 | 14 | # Screens 15 | 16 | ### Home page with BlazorCRUD features 17 | ![BlazorCrud Home Page](https://s3.amazonaws.com/s3.beckshome.com/20220213-blazorcrud-home.jpg) 18 | ### Login page 19 | ![BlazorCrud Login Page](https://s3.amazonaws.com/s3.beckshome.com/20220213-blazorcrud-login.jpg) 20 | ### Paginated results and search 21 | ![BlazorCrud Search Page](https://s3.amazonaws.com/s3.beckshome.com/20220213-blazorcrud-search.jpg) 22 | ### Edit page for complex objects with validations 23 | ![BlazorCrud Data Edit](https://s3.amazonaws.com/s3.beckshome.com/20220213-blazorcrud-edit.jpg) 24 | 25 | # Features 26 | 27 | * Online demo site to explore the application 28 | * CI/CD Using Azure DevOps 29 | * Docker container generation as a single deployable Docker image 30 | * Entity lists with pagination and search 31 | * Data entry forms with validations 32 | * Complex data entry with object graph validations 33 | * File upload and download using JavaScript Interop 34 | * Batch processing of JSON files 35 | * REST interfaces with Swagger documentation 36 | * Javascript Web Token (JWT) authentication 37 | * Data generation to pre-populate thousdands of entity records 38 | 39 | # Motivation and Credits 40 | 41 | I would not have got this project completed without the vast knowledge of experienced peers at my disposal. Referenced below are items that were specifically helpful with specific functional attributes of the solution. 42 | 43 | * [Blazor Navigation - Go Back](https://stackoverflow.com/questions/62561926/blazor-navigation-manager-go-back) 44 | * [Blazor Server Crud in VS Code](https://dev.to/rineshpk/blazor-server-crud-app-using-visual-studio-code-2b2g) 45 | * [Blazor Web Assembly - User Registration and Login](https://jasonwatmore.com/post/2020/11/09/blazor-webassembly-user-registration-and-login-example-tutorial#main-layout-razor) 46 | * [Building a Blazor Paging Component](https://gunnarpeipman.com/blazor-pager-component/) 47 | * [Integration Fluent Validation with Blazor](https://blog.stevensanderson.com/2019/09/04/blazor-fluentvalidation/) 48 | * [Lifelike Test Data Generation with Bogus](http://dontcodetired.com/blog/post/Lifelike-Test-Data-Generation-with-Bogus) 49 | * [User Registration and Login with Example API](https://jasonwatmore.com/post/2022/01/07/net-6-user-registration-and-login-tutorial-with-example-api#users-controller-cs) 50 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | name : dotnet-blazor-crud 2 | 3 | trigger: 4 | - master 5 | 6 | pool: 7 | vmImage: 'ubuntu-latest' 8 | 9 | variables: 10 | tag: '$(Build.BuildId)' 11 | workingDirectory: '$(System.DefaultWorkingDirectory)/' 12 | 13 | stages: 14 | - stage: Build 15 | displayName: Build image 16 | jobs: 17 | 18 | - job: Build 19 | displayName: Build and push docker image 20 | steps: 21 | - task: Docker@2 22 | displayName: Build and push the docker image 23 | inputs: 24 | command: buildAndPush 25 | containerRegistry: dockerHub 26 | repository: thbst16/dotnet-blazor-crud 27 | tags: | 28 | $(Build.BuildId) 29 | latest 30 | 31 | - stage: Deploy 32 | displayName: Deploy docker container 33 | dependsOn: Build 34 | jobs: 35 | 36 | - job: Deploy 37 | displayName: Deploy the container to Azure Container Apps 38 | steps: 39 | - task: AzureContainerApps@1 40 | displayName: Deploy the docker image 41 | inputs: 42 | azureSubscription: becksAzureRM 43 | containerAppName: dotnet-blazor-crud 44 | resourceGroup: beckshome-container-apps-rg 45 | imageToDeploy: thbst16/dotnet-blazor-crud:$(Build.BuildId) --------------------------------------------------------------------------------