├── .gitattributes ├── .gitignore ├── BlazorDB.sln ├── LICENSE ├── README.md ├── docs └── storageFormat.md └── src ├── BlazorDB ├── BlazorDB.csproj ├── BlazorDBUpdateException.cs ├── DataAnnotations │ ├── MaxLength.cs │ └── Required.cs ├── IStorageContext.cs ├── Options.cs ├── Properties │ └── launchSettings.json ├── ServiceCollectionExtensions.cs ├── Storage │ ├── BlazorDBLogger.cs │ ├── IBlazorDBInterop.cs │ ├── IBlazorDBLogger.cs │ ├── IStorageManager.cs │ ├── IStorageManagerLoad.cs │ ├── IStorageManagerSave.cs │ ├── IStorageManagerUtil.cs │ ├── Metadata.cs │ ├── SerializedModel.cs │ ├── StorageManager.cs │ ├── StorageManagerLoad.cs │ ├── StorageManagerSave.cs │ ├── StorageManagerUtil.cs │ ├── Util.cs │ └── blazorDBInterop.cs ├── StorageContext.cs ├── StorageSet.cs └── wwwroot │ └── blazorDBInterop.js └── Sample ├── App.razor ├── Models ├── Address.cs ├── AssociationContext.cs ├── Context.cs ├── Person.cs ├── State.cs ├── TodoContext.cs └── TodoItem.cs ├── Pages ├── Associations.razor ├── Index.razor ├── TodoItemForm.razor ├── Todos.razor └── _Imports.razor ├── Program.cs ├── Properties └── launchSettings.json ├── Sample.csproj ├── Shared ├── MainLayout.razor └── NavMenu.razor ├── Startup.cs ├── _Imports.razor └── wwwroot ├── css └── site.css └── index.html /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /BlazorDB.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.28809.33 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorDB", "src\BlazorDB\BlazorDB.csproj", "{3ED7CA17-E65D-48B9-A218-3EBFF0F9FD03}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "src\Sample\Sample.csproj", "{0C2A5B10-1805-41AB-B8CF-EFADD93BE494}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3ED7CA17-E65D-48B9-A218-3EBFF0F9FD03}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3ED7CA17-E65D-48B9-A218-3EBFF0F9FD03}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3ED7CA17-E65D-48B9-A218-3EBFF0F9FD03}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3ED7CA17-E65D-48B9-A218-3EBFF0F9FD03}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {0C2A5B10-1805-41AB-B8CF-EFADD93BE494}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {0C2A5B10-1805-41AB-B8CF-EFADD93BE494}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {0C2A5B10-1805-41AB-B8CF-EFADD93BE494}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {0C2A5B10-1805-41AB-B8CF-EFADD93BE494}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {43104ED8-856F-4BC9-AD67-6E5C9D2E2761} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlazorDB 2 | In memory, persisted to localstorage, database for .net Blazor browser framework 3 | 4 | ## Warning 5 | This library like Blazor itself is experimental and API is likely to change. 6 | 7 | ## Breaking change as of V0.7.0 8 | 9 | At this time, you will need to initialize the `Context` prior to using it. I hope that I cna get this done automatically again in a future version: 10 | 11 | ``` 12 | protected async override Task OnInitAsync() 13 | { 14 | await Context.Initialize(); 15 | } 16 | ``` 17 | 18 | ## Docs 19 | 20 | ### Install 21 | 22 | First add a reference to the nuget package: 23 | 24 | [![NuGet Pre Release](https://img.shields.io/nuget/vpre/BlazorDB.svg)](https://www.nuget.org/packages/BlazorDB/) 25 | 26 | Then in `Startup.cs` in the method `ConfigureServices` add Blazor DB to the dependency injection services: 27 | 28 | ``` 29 | public void ConfigureServices(IServiceCollection services) 30 | { 31 | services.AddBlazorDB(options => 32 | { 33 | options.LogDebug = true; 34 | options.Assembly = typeof(Program).Assembly; 35 | }); 36 | } 37 | ``` 38 | Set `LogDebug` to see debug output in the browser console. 39 | 40 | ### Setup 41 | 42 | **NOTE:** Models stored by BlazorDB require that an int Id property exist on the model. The Id property will be maintained by BlazorDB, you dont need to set it yourself. 43 | 44 | Create at least one model and context for example: 45 | 46 | Person.cs: 47 | 48 | ``` 49 | public class Person 50 | { 51 | public int Id { get; set; } 52 | public string FirstName { get; set; } 53 | public string LastName { get; set; } 54 | public Address HomeAddress { get; set; } 55 | } 56 | ``` 57 | 58 | if the field `public int Id { get; set; }` exists it will be managed by BlazorDB 59 | 60 | and a context, for example, Context.cs: 61 | ``` 62 | public class Context : StorageContext 63 | { 64 | public StorageSet People { get; set; } 65 | } 66 | ``` 67 | 68 | ### Usage 69 | 70 | See the full example in the sample app: https://github.com/chanan/BlazorDB/blob/master/src/Sample/Pages/Index.razor 71 | 72 | Inject your context into your component: 73 | 74 | ``` 75 | @using Sample.Models 76 | @inject Context Context 77 | ``` 78 | 79 | Currently, as of v0.7.0, before using the `Context` object you must initialize it. Hopefully, this requirement will go away in a future version: 80 | 81 | ``` 82 | protected async override Task OnInitAsync() 83 | { 84 | await Context.Initialize(); 85 | } 86 | ``` 87 | 88 | Create a model and add it to your Context: 89 | 90 | ``` 91 | var person = new Person { FirstName = "John", LastName = "Smith" }; 92 | Context.People.Add(person); 93 | ``` 94 | 95 | Do not set the Id field. It will be assigned by BlazorDB. 96 | 97 | Call SaveChanges: 98 | 99 | ``` 100 | Context.SaveChanges(); 101 | ``` 102 | 103 | Once `SaveChanges()` has been called, you may close the browser and return later, the data will be reloaded from localStorage. 104 | 105 | You may query the data using linq for example: 106 | 107 | ``` 108 | private Person Person { get; set; } 109 | void onclickGetPerson() 110 | { 111 | var query = from person in Context.People 112 | where person.Id == 1 113 | select person; 114 | Person = query.Single(); 115 | StateHasChanged(); 116 | } 117 | ``` 118 | 119 | ## Associations 120 | 121 | Associations work in the same context. If you have an object in another object that is not in the context, it will be serialized to localStorage as one "complex" document. 122 | 123 | For example, in `Context.cs` only Person is in the Context and Address is not. Therefore, Person will contain Address, and Address will not be a seperate object. 124 | 125 | ### One to One Association 126 | 127 | When an object refers to another object that are both in Context, they are stored as a reference, such that changing the reference will update both objects. 128 | 129 | For example, `AssociationContext.cs`: 130 | 131 | 132 | ``` 133 | public class AssociationContext : StorageContext 134 | { 135 | public StorageSet People { get; set; } 136 | public StorageSet
Addresses { get; set; } 137 | } 138 | ``` 139 | 140 | `Person.cs` as shown above has a property `public Address HomeAddress { get; set; }`. Because unlike `Context.cs`, `AssociationContext.cs` does define `public StorageSet
Addresses { get; set; }` references are stored as "foreign keys" instead of complex objects. 141 | 142 | Therefore, like in `Associations.cshtml` example, changing the Address will Change the Person's HomeAddress: 143 | 144 | ``` 145 | Context.People[0].HomeAddress.Street = "Changed Streeet"; 146 | Context.SaveChanges(); 147 | Console.WriteLine("Person address changed: {0}", Context.People[0].HomeAddress.Street); 148 | Console.WriteLine("Address entity changed as well: {0}", Context.Addresses[0].Street); 149 | StateHasChanged(); 150 | ``` 151 | ### One to Many, Many to Many Association 152 | 153 | Define a "One" association by adding a property of the other model. For example in `Person.cs`: 154 | 155 | ``` 156 | public Address HomeAddress { get; set; } 157 | ``` 158 | 159 | Define a "Many" association by adding a property of type `List<>` to the association. For example in `Person.cs`: 160 | 161 | ``` 162 | public List
OtherAddresses { get; set; } 163 | ``` 164 | 165 | This is association is then used in `Associations.cshtml` like so: 166 | 167 | ``` 168 | var person = new Person { FirstName = "Many", LastName = "Test" }; 169 | person.HomeAddress = new Address { Street = "221 Baker Streeet", City = "This should be a refrence to address since Address exists in the context" }; 170 | var address1 = new Address { Street = "Many test 1", City = "Saved as a reference" }; 171 | var address2 = new Address { Street = "Many test 2", City = "Saved as a reference" }; 172 | person.OtherAddresses = new List
{ address1, address2 }; 173 | Context.People.Add(person); 174 | Context.SaveChanges(); 175 | StateHasChanged(); 176 | ``` 177 | 178 | ### Maintaining Associations 179 | 180 | As you can see in the example above BlazorDB will detect associations added to the model so no need to add them to the Context explicitly. In the example above, the address objects do not need to be explicitly added to the context, instead they are persisted when the person object is added and `SaveChanges()` is called. 181 | 182 | **Note:** At this time removing/deleting is not done automatically and needs to be done manually. A future update of BlazorDB will handle deletions properly. 183 | 184 | ## Validations 185 | 186 | You can annotate your model's propeties with `[Required]` and `[MaxLength(int)]` to enforce required and max length on properties. 187 | 188 | ## Example 189 | 190 | A Todo sample built with BlazorDB is included in the sample project: 191 | 192 | * [Todos.razor](https://github.com/chanan/BlazorDB/blob/master/src/Sample/Pages/Todos.razor) 193 | * [TodoItemForm.razor](https://github.com/chanan/BlazorDB/blob/master/src/Sample/Pages/TodoItemForm.razor) 194 | 195 | ## Fluxor Integration Example 196 | 197 | The [Fluxor integration example](https://github.com/chanan/BlazorDB/tree/master/src/FluxorIntegration) shows how to use BlazorDB to manage data and Fluxor to connect between the UI and the data layer. 198 | 199 | ## Storage Format 200 | 201 | [Storage Format Doc](https://github.com/chanan/BlazorDB/blob/master/docs/storageFormat.md) 202 | -------------------------------------------------------------------------------- /docs/storageFormat.md: -------------------------------------------------------------------------------- 1 | # Storage Format 2 | 3 | ## Models 4 | 5 | `{FQN context class}-{FQN model class}-{Guid}` 6 | 7 | Contents: 8 | 9 | * Each file stores the json serialized output of the model. One "row". 10 | 11 | ### Associations 12 | 13 | **One Association** 14 | 15 | For objects that contain other object not in the same context, they will be serialized as one object, for example (`Context.cs`): 16 | 17 | ``` 18 | Person with an Address: 19 | {"Id":1,"FirstName":"John","LastName":"Smith","HomeAddress":{"Id":0,"Street":"221 Baker Street","City":"This should be part of the json, since address is not in context (Very long city)"}} 20 | ``` 21 | 22 | For objects that contain object in the same context, they will be serialzied with a "foreign key", for example (`AssociationContext.cs`): 23 | 24 | ``` 25 | Address: 26 | {"Id":1,"Street":"Changed Streeet","City":"This should be a refrence to address since Address exists in the context"} 27 | 28 | Person: 29 | {"Id":1,"FirstName":"John","LastName":"Smith","HomeAddress":1} 30 | 31 | ``` 32 | 33 | **Many Association** 34 | 35 | Many associations are stored as an array of ids: 36 | 37 | ``` 38 | {"Id":1,"FirstName":"Many","LastName":"Test","HomeAddress":null,"OtherAddresses":[1,2]} 39 | ``` 40 | 41 | ## Metadata 42 | 43 | `{FQN context class}-{FQN model class}-{metadata}` 44 | 45 | Contents: 46 | 47 | * Guids - List of persisted guids 48 | * MaxId - The last id of the StorageSet 49 | 50 | Initial implementation will regenerate the guid on every `SaveChanges()` and the list in the metadata table. Future implementation might store metadata about the model in the model value itself, so the guid will be loaded into memory and won't be regenerated. 51 | 52 | Future items that might be stored in the the metadata file would be items such as index infromation -------------------------------------------------------------------------------- /src/BlazorDB/BlazorDB.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; 7 | https://dotnet.myget.org/F/blazor-dev/api/v3/index.json; 8 | 9 | Library 10 | true 11 | false 12 | 7.3 13 | 3.0 14 | BlazorDB 15 | 1.0.0-preview7 16 | Chanan Braunstein 17 | Blazor localStorage Database 18 | In memory, persisted to localstorage, database for .net Blazor browser framework 19 | blazor;database;linq;localstorage 20 | https://github.com/chanan/BlazorDB 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/BlazorDB/BlazorDBUpdateException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorDB 4 | { 5 | public class BlazorDBUpdateException : Exception 6 | { 7 | public BlazorDBUpdateException(string error) : base(error) 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/BlazorDB/DataAnnotations/MaxLength.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorDB.DataAnnotations 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public class MaxLength : Attribute 7 | { 8 | public MaxLength(int length) 9 | { 10 | this.length = length; 11 | } 12 | 13 | internal int length; 14 | } 15 | } -------------------------------------------------------------------------------- /src/BlazorDB/DataAnnotations/Required.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorDB.DataAnnotations 4 | { 5 | [AttributeUsage(AttributeTargets.Property)] 6 | public class Required : Attribute 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/BlazorDB/IStorageContext.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BlazorDB 4 | { 5 | public interface IStorageContext 6 | { 7 | Task SaveChanges(); 8 | Task LogToConsole(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BlazorDB/Options.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace BlazorDB 4 | { 5 | public class Options 6 | { 7 | public Assembly Assembly { get; set; } 8 | public bool LogDebug { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /src/BlazorDB/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:56678/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "BlazorDB": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:56679/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/BlazorDB/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB.Storage; 2 | using BlazorLogger; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.JSInterop; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Reflection; 9 | 10 | namespace BlazorDB 11 | { 12 | public static class ServiceCollectionExtensions 13 | { 14 | private static readonly Type StorageContext = typeof(StorageContext); 15 | 16 | public static IServiceCollection AddBlazorDB(this IServiceCollection serviceCollection, 17 | Action configure) 18 | { 19 | if (configure == null) 20 | { 21 | throw new ArgumentNullException(nameof(configure)); 22 | } 23 | 24 | Options options = new Options(); 25 | configure(options); 26 | if (options.LogDebug) 27 | { 28 | BlazorDBLogger.LogDebug = true; 29 | } 30 | 31 | Scan(serviceCollection, options.Assembly); 32 | return serviceCollection; 33 | } 34 | 35 | private static void Scan(IServiceCollection serviceCollection, Assembly assembly) 36 | { 37 | IEnumerable types = ScanForContexts(serviceCollection, assembly); 38 | serviceCollection.AddJavascriptLogger(); 39 | serviceCollection.AddSingleton(); 40 | serviceCollection.AddSingleton(); 41 | serviceCollection.AddSingleton(); 42 | serviceCollection.AddSingleton(); 43 | serviceCollection.AddSingleton(); 44 | serviceCollection.AddSingleton(); 45 | 46 | foreach (Type type in types) 47 | { 48 | serviceCollection.AddSingleton(type, s => 49 | { 50 | IJSRuntime jsRuntime = s.GetRequiredService(); 51 | object instance = Activator.CreateInstance(type); 52 | PropertyInfo smProp = type.GetProperty("StorageManager", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); 53 | IStorageManager storageManager = s.GetRequiredService(); 54 | smProp.SetValue(instance, storageManager); 55 | 56 | PropertyInfo lProp = type.GetProperty("Logger", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); 57 | IBlazorDBLogger logger = s.GetRequiredService(); 58 | lProp.SetValue(instance, logger); 59 | 60 | PropertyInfo smuProp = type.GetProperty("StorageManagerUtil", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); 61 | IStorageManagerUtil storageManagerUtil = s.GetRequiredService(); 62 | smuProp.SetValue(instance, storageManagerUtil); 63 | return instance; 64 | }); 65 | } 66 | } 67 | 68 | private static IEnumerable ScanForContexts(IServiceCollection serviceCollection, Assembly assembly) 69 | { 70 | IEnumerable types = assembly.GetTypes() 71 | .Where(x => StorageContext.IsAssignableFrom(x)) 72 | .ToList(); 73 | return types; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/BlazorDBLogger.cs: -------------------------------------------------------------------------------- 1 | using BlazorLogger; 2 | using System; 3 | using System.Threading.Tasks; 4 | 5 | namespace BlazorDB.Storage 6 | { 7 | internal class BlazorDBLogger : IBlazorDBLogger 8 | { 9 | private const string Blue = "color: blue; font-style: bold;"; 10 | private const string Green = "color: green; font-style: bold;"; 11 | private const string Red = "color: red; font-style: bold;"; 12 | private const string Normal = "color: black; font-style: normal;"; 13 | internal static bool LogDebug { get; set; } = true; 14 | 15 | private readonly ILogger _logger; 16 | public BlazorDBLogger(ILogger logger) 17 | { 18 | _logger = logger; 19 | } 20 | 21 | public async Task LogStorageSetToConsole(Type type, object list) 22 | { 23 | if (!LogDebug) 24 | { 25 | return; 26 | } 27 | 28 | await _logger.Log($"StorageSet<{type.GetGenericArguments()[0].Name}>: %o", list); 29 | } 30 | 31 | public async Task StartContextType(Type contextType, bool loading = true) 32 | { 33 | if (!LogDebug) 34 | { 35 | return; 36 | } 37 | 38 | string message = loading ? "loading" : "log"; 39 | await _logger.GroupCollapsed($"Context {message}: %c{contextType.Namespace}.{contextType.Name}", Blue); 40 | } 41 | 42 | public async Task ContextSaved(Type contextType) 43 | { 44 | if (!LogDebug) 45 | { 46 | return; 47 | } 48 | 49 | await _logger.GroupCollapsed($"Context %csaved: %c{contextType.Namespace}.{contextType.Name}", Green, 50 | Blue); 51 | } 52 | 53 | public void StorageSetSaved(Type modelType, int count) 54 | { 55 | if (!LogDebug) 56 | { 57 | return; 58 | } 59 | 60 | _logger.Log( 61 | $"StorageSet %csaved: %c{modelType.Namespace}.{modelType.Name}%c with {count} items", Green, Blue, 62 | Normal); 63 | } 64 | 65 | public void EndGroup() 66 | { 67 | if (!LogDebug) 68 | { 69 | return; 70 | } 71 | 72 | _logger.GroupEnd(); 73 | } 74 | 75 | public void ItemAddedToContext(string contextTypeName, Type modelType, object item) 76 | { 77 | if (!LogDebug) 78 | { 79 | return; 80 | } 81 | 82 | _logger.GroupCollapsed( 83 | $"Item %c{modelType.Namespace}.{modelType.Name}%c %cadded%c to context: %c{contextTypeName}", Blue, 84 | Normal, Green, Normal, Blue); 85 | _logger.Log("Item: %o", item); 86 | _logger.GroupEnd(); 87 | } 88 | 89 | public void LoadModelInContext(Type modelType, int count) 90 | { 91 | if (!LogDebug) 92 | { 93 | return; 94 | } 95 | 96 | _logger.Log( 97 | $"StorageSet loaded: %c{modelType.Namespace}.{modelType.Name}%c with {count} items", Blue, Normal); 98 | } 99 | 100 | public void ItemRemovedFromContext(string contextTypeName, Type modelType) 101 | { 102 | if (!LogDebug) 103 | { 104 | return; 105 | } 106 | 107 | _logger.Log( 108 | $"Item %c{modelType.Namespace}.{modelType.Name}%c %cremoved%c from context: %c{contextTypeName}", Blue, 109 | Normal, Red, Normal, Blue); 110 | } 111 | 112 | public void Error(string error) 113 | { 114 | //Always log errors 115 | _logger.Error(error); 116 | } 117 | } 118 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/IBlazorDBInterop.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BlazorDB.Storage 4 | { 5 | internal interface IBlazorDBInterop 6 | { 7 | Task Clear(bool session); 8 | Task GetItem(string key, bool session); 9 | Task Log(params object[] list); 10 | Task RemoveItem(string key, bool session); 11 | Task SetItem(string key, string value, bool session); 12 | } 13 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/IBlazorDBLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace BlazorDB.Storage 5 | { 6 | public interface IBlazorDBLogger 7 | { 8 | Task ContextSaved(Type contextType); 9 | void EndGroup(); 10 | void ItemAddedToContext(string contextTypeName, Type modelType, object item); 11 | void ItemRemovedFromContext(string contextTypeName, Type modelType); 12 | void LoadModelInContext(Type modelType, int count); 13 | Task LogStorageSetToConsole(Type type, object list); 14 | Task StartContextType(Type contextType, bool loading = true); 15 | void StorageSetSaved(Type modelType, int count); 16 | void Error(string error); 17 | } 18 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/IStorageManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BlazorDB.Storage 4 | { 5 | public interface IStorageManager 6 | { 7 | Task SaveContextToLocalStorage(StorageContext context); 8 | Task LoadContextFromLocalStorage(StorageContext context); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BlazorDB/Storage/IStorageManagerLoad.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BlazorDB.Storage 4 | { 5 | internal interface IStorageManagerLoad 6 | { 7 | Task LoadContextFromLocalStorage(StorageContext context); 8 | } 9 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/IStorageManagerSave.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BlazorDB.Storage 4 | { 5 | internal interface IStorageManagerSave 6 | { 7 | Task SaveContextToLocalStorage(StorageContext context); 8 | } 9 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/IStorageManagerUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Threading.Tasks; 5 | 6 | namespace BlazorDB.Storage 7 | { 8 | public interface IStorageManagerUtil 9 | { 10 | List GetStorageSets(Type contextType); 11 | bool IsInContext(List storageSets, PropertyInfo prop); 12 | bool IsListInContext(List storageSets, PropertyInfo prop); 13 | Task LoadMetadata(string storageTableName); 14 | string ReplaceString(string source, int start, int end, string stringToInsert); 15 | } 16 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/Metadata.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace BlazorDB.Storage 5 | { 6 | public class Metadata 7 | { 8 | public List Guids { get; set; } 9 | public string ModelName { get; set; } 10 | public string ContextName { get; set; } 11 | public int MaxId { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/SerializedModel.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorDB.Storage 2 | { 3 | internal class SerializedModel 4 | { 5 | public bool ScanDone { get; set; } 6 | public bool HasAssociation { get; set; } 7 | public string StringModel { get; set; } 8 | public object Model { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/BlazorDB/Storage/StorageManager.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace BlazorDB.Storage 4 | { 5 | internal class StorageManager : IStorageManager 6 | { 7 | private readonly IStorageManagerLoad _storageManagerLoad; 8 | private readonly IStorageManagerSave _storageManagerSave; 9 | 10 | public StorageManager(IStorageManagerSave storageManagerSave, IStorageManagerLoad storageManagerLoad) 11 | { 12 | _storageManagerSave = storageManagerSave; 13 | _storageManagerLoad = storageManagerLoad; 14 | 15 | } 16 | 17 | public Task SaveContextToLocalStorage(StorageContext context) 18 | { 19 | return _storageManagerSave.SaveContextToLocalStorage(context); 20 | } 21 | 22 | public Task LoadContextFromLocalStorage(StorageContext context) 23 | { 24 | return _storageManagerLoad.LoadContextFromLocalStorage(context); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/StorageManagerLoad.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Threading.Tasks; 8 | 9 | namespace BlazorDB.Storage 10 | { 11 | internal class StorageManagerLoad : IStorageManagerLoad 12 | { 13 | private readonly IBlazorDBLogger _logger; 14 | private readonly IBlazorDBInterop _blazorDBInterop; 15 | private readonly IStorageManagerUtil _storageManagerUtil; 16 | public StorageManagerLoad(IBlazorDBLogger logger, IBlazorDBInterop blazorDBInterop, IStorageManagerUtil storageManagerUtil) 17 | { 18 | _logger = logger; 19 | _blazorDBInterop = blazorDBInterop; 20 | _storageManagerUtil = storageManagerUtil; 21 | } 22 | 23 | public async Task LoadContextFromLocalStorage(StorageContext context) 24 | { 25 | Type contextType = context.GetType(); 26 | await _logger.StartContextType(contextType); 27 | List storageSets = _storageManagerUtil.GetStorageSets(contextType); 28 | Dictionary> stringModels = await LoadStringModels(contextType, storageSets); 29 | //PrintStringModels(stringModels); 30 | stringModels = ScanNonAssociationModels(storageSets, stringModels); 31 | stringModels = ScanAssociationModels(storageSets, stringModels); 32 | stringModels = DeserializeModels(stringModels, storageSets); 33 | //PrintStringModels(stringModels); 34 | await EnrichContext(context, contextType, stringModels); 35 | _logger.EndGroup(); 36 | } 37 | 38 | private async Task EnrichContext(StorageContext context, Type contextType, 39 | IReadOnlyDictionary> stringModels) 40 | { 41 | foreach (PropertyInfo prop in contextType.GetProperties()) 42 | { 43 | if (prop.PropertyType.IsGenericType && 44 | prop.PropertyType.GetGenericTypeDefinition() == typeof(StorageSet<>)) 45 | { 46 | Type modelType = prop.PropertyType.GetGenericArguments()[0]; 47 | Type storageSetType = StorageManagerUtil.GenericStorageSetType.MakeGenericType(modelType); 48 | string storageTableName = Util.GetStorageTableName(contextType, modelType); 49 | Metadata metadata = await _storageManagerUtil.LoadMetadata(storageTableName); 50 | if (stringModels.ContainsKey(modelType)) 51 | { 52 | Dictionary map = stringModels[modelType]; 53 | _logger.LoadModelInContext(modelType, map.Count); 54 | } 55 | else 56 | { 57 | _logger.LoadModelInContext(modelType, 0); 58 | } 59 | object storageSet = metadata != null 60 | ? LoadStorageSet(storageSetType, contextType, modelType, stringModels[modelType]) 61 | : CreateNewStorageSet(storageSetType, contextType); 62 | prop.SetValue(context, storageSet); 63 | } 64 | } 65 | } 66 | 67 | private Dictionary> DeserializeModels( 68 | Dictionary> stringModels, List storageSets) 69 | { 70 | foreach (KeyValuePair> map in stringModels) 71 | { 72 | Type modelType = map.Key; 73 | foreach (KeyValuePair sm in map.Value) 74 | { 75 | SerializedModel stringModel = sm.Value; 76 | if (!stringModel.HasAssociation) 77 | { 78 | stringModel.Model = DeserializeModel(modelType, stringModel.StringModel); 79 | } 80 | } 81 | } 82 | 83 | foreach (KeyValuePair> map in stringModels) //TODO: Fix associations that are more than one level deep 84 | { 85 | Type modelType = map.Key; 86 | foreach (KeyValuePair sm in map.Value) 87 | { 88 | SerializedModel stringModel = sm.Value; 89 | if (stringModel.Model != null) 90 | { 91 | continue; 92 | } 93 | 94 | object model = DeserializeModel(modelType, stringModel.StringModel); 95 | foreach (PropertyInfo prop in model.GetType().GetProperties()) 96 | { 97 | if (_storageManagerUtil.IsInContext(storageSets, prop) && prop.GetValue(model) != null) 98 | { 99 | object associatedLocalModel = prop.GetValue(model); 100 | PropertyInfo localIdProp = 101 | associatedLocalModel.GetType() 102 | .GetProperty(StorageManagerUtil.Id); 103 | if (localIdProp == null) 104 | { 105 | throw new ArgumentException("Model must have Id property"); 106 | } 107 | 108 | int localId = Convert.ToInt32(localIdProp.GetValue(associatedLocalModel)); 109 | object associatdRemoteModel = 110 | GetModelFromStringModels(stringModels, associatedLocalModel.GetType(), localId) 111 | .Model; 112 | prop.SetValue(model, associatdRemoteModel); 113 | } 114 | } 115 | 116 | stringModel.Model = model; 117 | } 118 | } 119 | 120 | return stringModels; 121 | } 122 | 123 | private static SerializedModel GetModelFromStringModels( 124 | IReadOnlyDictionary> stringModels, Type type, int localId) 125 | { 126 | return stringModels[type][localId]; 127 | } 128 | 129 | private object LoadStorageSet(Type storageSetType, Type contextType, Type modelType, 130 | Dictionary map) 131 | { 132 | object instance = CreateNewStorageSet(storageSetType, contextType); 133 | Type listGenericType = StorageManagerUtil.GenericListType.MakeGenericType(modelType); 134 | object list = Activator.CreateInstance(listGenericType); 135 | foreach (KeyValuePair sm in map) 136 | { 137 | SerializedModel stringModel = sm.Value; 138 | MethodInfo addMethod = listGenericType.GetMethod(StorageManagerUtil.Add); 139 | addMethod.Invoke(list, new[] { stringModel.Model }); 140 | } 141 | 142 | return SetList(instance, list); 143 | } 144 | 145 | private Dictionary> ScanNonAssociationModels( 146 | List storageSets, Dictionary> stringModels) 147 | { 148 | foreach (KeyValuePair> map in stringModels) 149 | { 150 | Type modelType = map.Key; 151 | foreach (KeyValuePair sm in map.Value) 152 | { 153 | SerializedModel stringModel = sm.Value; 154 | if (!HasAssociation(storageSets, modelType) && 155 | !HasListAssociation(storageSets, modelType)) 156 | { 157 | stringModel.HasAssociation = false; 158 | stringModel.ScanDone = true; 159 | } 160 | else 161 | { 162 | stringModel.HasAssociation = true; 163 | } 164 | } 165 | } 166 | 167 | return stringModels; 168 | } 169 | 170 | private bool HasAssociation(List storageSets, Type modelType) 171 | { 172 | return modelType.GetProperties().Any(prop => _storageManagerUtil.IsInContext(storageSets, prop)); 173 | } 174 | 175 | private bool HasListAssociation(List storageSets, Type modelType) 176 | { 177 | return modelType.GetProperties().Any(prop => _storageManagerUtil.IsListInContext(storageSets, prop)); 178 | } 179 | 180 | 181 | //TODO: The snippet below should also check to see that the model itself has no more associations to fix, not just if it has properties. 182 | /* 183 | * if(HasAssociation(storageSets, modelType, stringModel)) 184 | { 185 | stringModel.StringModel = FixAssociationsInStringModels(stringModel, modelType, storageSets, stringModels); 186 | stringModel.ScanDone = true; 187 | }*/ 188 | private Dictionary> ScanAssociationModels( 189 | List storageSets, 190 | Dictionary> stringModels) 191 | { 192 | int count = 0; 193 | do 194 | { 195 | count++; 196 | foreach (KeyValuePair> map in stringModels) 197 | { 198 | Type modelType = map.Key; 199 | foreach (KeyValuePair sm in map.Value) 200 | { 201 | SerializedModel stringModel = sm.Value; 202 | if (stringModel.ScanDone) 203 | { 204 | continue; 205 | } 206 | 207 | if (HasAssociation(storageSets, modelType) || 208 | HasListAssociation(storageSets, modelType)) 209 | { 210 | stringModel.StringModel = 211 | FixAssociationsInStringModels(stringModel, modelType, storageSets, stringModels); 212 | stringModel.ScanDone = true; 213 | } 214 | else 215 | { 216 | stringModel.ScanDone = true; 217 | } 218 | } 219 | } 220 | 221 | if (count == 20) 222 | { 223 | break; //Go 20 deep throw exception here? 224 | } 225 | } while (IsScanDone(stringModels)); 226 | 227 | return stringModels; 228 | } 229 | 230 | private void PrintStringModels(Dictionary> stringModels) 231 | { 232 | foreach (KeyValuePair> map in stringModels) 233 | { 234 | Type modelType = map.Key; 235 | Console.WriteLine("-----------"); 236 | Console.WriteLine("modelType: {0}", modelType.Name); 237 | foreach (KeyValuePair sm in map.Value) 238 | { 239 | Console.WriteLine("Key: {0}", sm.Key); 240 | Console.WriteLine("sm: {0}", sm.Value.StringModel); 241 | Console.WriteLine("Is Done: {0}", sm.Value.ScanDone); 242 | Console.WriteLine("Has Model: {0}", sm.Value.Model != null); 243 | } 244 | } 245 | } 246 | 247 | private string FixAssociationsInStringModels(SerializedModel stringModel, Type modelType, 248 | List storageSets, Dictionary> stringModels) 249 | { 250 | string result = stringModel.StringModel; 251 | foreach (PropertyInfo prop in modelType.GetProperties()) 252 | { 253 | if (_storageManagerUtil.IsInContext(storageSets, prop) && 254 | TryGetIdFromSerializedModel(result, prop.Name, out int id)) 255 | { 256 | string updated = GetAssociatedStringModel(stringModels, prop.PropertyType, id); 257 | result = ReplaceIdWithAssociation(result, prop.Name, id, updated); 258 | } 259 | 260 | if (!_storageManagerUtil.IsListInContext(storageSets, prop)) 261 | { 262 | continue; 263 | } 264 | 265 | { 266 | if (!TryGetIdListFromSerializedModel(result, prop.Name, out List idList)) 267 | { 268 | continue; 269 | } 270 | 271 | StringBuilder sb = new StringBuilder(); 272 | foreach (int item in idList) 273 | { 274 | string updated = GetAssociatedStringModel(stringModels, 275 | prop.PropertyType.GetGenericArguments()[0], item); 276 | sb.Append(updated).Append(","); 277 | } 278 | 279 | string strList = sb.ToString().Substring(0, sb.ToString().Length - 1); 280 | result = ReplaceListWithAssociationList(result, prop.Name, strList); 281 | } 282 | } 283 | 284 | return result; 285 | } 286 | 287 | private string ReplaceListWithAssociationList(string serializedModel, string propName, string strList) 288 | { 289 | int propStart = serializedModel.IndexOf($"\"{propName}\":[", StringComparison.Ordinal); 290 | int start = serializedModel.IndexOf('[', propStart) + 1; 291 | int end = serializedModel.IndexOf(']', start); 292 | string result = _storageManagerUtil.ReplaceString(serializedModel, start, end, strList); 293 | return result; 294 | } 295 | 296 | private static bool TryGetIdListFromSerializedModel(string serializedModel, string propName, 297 | out List idList) 298 | { 299 | List list = new List(); 300 | if (serializedModel.IndexOf($"\"{propName}\":null", StringComparison.Ordinal) != -1) 301 | { 302 | idList = list; 303 | return false; 304 | } 305 | 306 | int propStart = serializedModel.IndexOf($"\"{propName}\":[", StringComparison.Ordinal); 307 | int start = serializedModel.IndexOf('[', propStart) + 1; 308 | int end = serializedModel.IndexOf(']', start); 309 | string stringlist = serializedModel.Substring(start, end - start); 310 | string[] arr = stringlist.Split(','); 311 | list.AddRange(arr.Select(s => Convert.ToInt32(s))); 312 | idList = list; 313 | return true; 314 | } 315 | 316 | private static string GetAssociatedStringModel( 317 | IReadOnlyDictionary> stringModels, 318 | Type modelType, int id) 319 | { 320 | Dictionary map = stringModels[modelType]; 321 | return map[id].StringModel; 322 | } 323 | 324 | //TODO: Convert to Linq 325 | private static bool IsScanDone(Dictionary> stringModels) 326 | { 327 | bool done = true; 328 | foreach (Dictionary map in stringModels.Values) 329 | { 330 | foreach (SerializedModel sm in map.Values) 331 | { 332 | if (!sm.ScanDone) 333 | { 334 | done = false; 335 | } 336 | } 337 | } 338 | 339 | return done; 340 | } 341 | 342 | private async Task>> LoadStringModels(Type contextType, 343 | IEnumerable storageSets) 344 | { 345 | Dictionary> stringModels = new Dictionary>(); 346 | foreach (PropertyInfo prop in storageSets) 347 | { 348 | Type modelType = prop.PropertyType.GetGenericArguments()[0]; 349 | Dictionary map = new Dictionary(); 350 | string storageTableName = Util.GetStorageTableName(contextType, modelType); 351 | Metadata metadata = await _storageManagerUtil.LoadMetadata(storageTableName); 352 | if (metadata == null) 353 | { 354 | continue; 355 | } 356 | 357 | foreach (Guid guid in metadata.Guids) 358 | { 359 | string name = $"{storageTableName}-{guid}"; 360 | string serializedModel = await _blazorDBInterop.GetItem(name, false); 361 | int id = FindIdInSerializedModel(serializedModel); 362 | map.Add(id, new SerializedModel { StringModel = serializedModel }); 363 | } 364 | 365 | stringModels.Add(modelType, map); 366 | } 367 | 368 | return stringModels; 369 | } 370 | 371 | //TODO: Verify that the found id is at the top level in case of nested objects 372 | private static bool TryGetIdFromSerializedModel(string serializedModel, string propName, out int id) 373 | { 374 | if (serializedModel.IndexOf($"\"{propName}\":null", StringComparison.Ordinal) != -1) 375 | { 376 | id = -1; 377 | return false; 378 | } 379 | 380 | int propStart = serializedModel.IndexOf($"\"{propName}\":", StringComparison.Ordinal); 381 | int start = serializedModel.IndexOf(':', propStart); 382 | id = GetIdFromString(serializedModel, start); 383 | return true; 384 | } 385 | 386 | //TODO: Verify that the found id is at the top level in case of nested objects 387 | private static int FindIdInSerializedModel(string serializedModel) 388 | { 389 | int start = serializedModel.IndexOf($"\"{StorageManagerUtil.Id}\":", StringComparison.Ordinal); 390 | return GetIdFromString(serializedModel, start); 391 | } 392 | 393 | private static int GetIdFromString(string stringToSearch, int startFrom = 0) 394 | { 395 | Console.WriteLine("stringToSearch: " + stringToSearch); 396 | Console.WriteLine("startFrom: " + startFrom); 397 | bool foundFirst = false; 398 | char[] arr = stringToSearch.ToCharArray(); 399 | List result = new List(); 400 | for (int i = startFrom; i < arr.Length; i++) 401 | { 402 | char ch = arr[i]; 403 | Console.WriteLine("ch: " + ch); 404 | if (char.IsDigit(ch)) 405 | { 406 | foundFirst = true; 407 | result.Add(ch); 408 | } 409 | else 410 | { 411 | if (foundFirst) 412 | { 413 | break; 414 | } 415 | } 416 | } 417 | 418 | return Convert.ToInt32(new string(result.ToArray())); 419 | } 420 | 421 | private string ReplaceIdWithAssociation(string result, string name, int id, string stringModel) 422 | { 423 | string stringToFind = $"\"{name}\":{id}"; 424 | int nameIndex = result.IndexOf(stringToFind, StringComparison.Ordinal); 425 | int index = result.IndexOf(id.ToString(), nameIndex, StringComparison.Ordinal); 426 | result = _storageManagerUtil.ReplaceString(result, index, index + id.ToString().Length, stringModel); 427 | return result; 428 | } 429 | 430 | private object CreateNewStorageSet(Type storageSetType, Type contextType) 431 | { 432 | object instance = Activator.CreateInstance(storageSetType); 433 | PropertyInfo prop = storageSetType.GetProperty(StorageManagerUtil.StorageContextTypeName, 434 | StorageManagerUtil.Flags); 435 | prop.SetValue(instance, Util.GetFullyQualifiedTypeName(contextType)); 436 | 437 | PropertyInfo lProp = storageSetType.GetProperty("Logger", StorageManagerUtil.Flags); 438 | lProp.SetValue(instance, _logger); 439 | 440 | return instance; 441 | } 442 | 443 | private static object SetList(object instance, object list) 444 | { 445 | PropertyInfo prop = instance.GetType().GetProperty(StorageManagerUtil.List, StorageManagerUtil.Flags); 446 | prop.SetValue(instance, list); 447 | return instance; 448 | } 449 | 450 | private static object DeserializeModel(Type modelType, string value) 451 | { 452 | MethodInfo method = typeof(JsonWrapper).GetMethod(StorageManagerUtil.Deserialize); 453 | MethodInfo genericMethod = method.MakeGenericMethod(modelType); 454 | object model = genericMethod.Invoke(new JsonWrapper(), new object[] { value }); 455 | return model; 456 | } 457 | } 458 | 459 | internal class JsonWrapper 460 | { 461 | public T Deserialize(string value) 462 | { 463 | return JsonSerializer.Deserialize(value); 464 | } 465 | } 466 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/StorageManagerSave.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB.DataAnnotations; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Text.Json; 8 | using System.Threading.Tasks; 9 | 10 | namespace BlazorDB.Storage 11 | { 12 | internal class StorageManagerSave : IStorageManagerSave 13 | { 14 | private readonly IBlazorDBLogger _logger; 15 | private readonly IBlazorDBInterop _blazorDBInterop; 16 | private readonly IStorageManagerUtil _storageManagerUtil; 17 | public StorageManagerSave(IBlazorDBLogger logger, IBlazorDBInterop blazorDBInterop, IStorageManagerUtil storageManagerUtil) 18 | { 19 | _logger = logger; 20 | _blazorDBInterop = blazorDBInterop; 21 | _storageManagerUtil = storageManagerUtil; 22 | } 23 | 24 | public async Task SaveContextToLocalStorage(StorageContext context) 25 | { 26 | int total = 0; 27 | Type contextType = context.GetType(); 28 | await _logger.ContextSaved(contextType); 29 | List storageSets = _storageManagerUtil.GetStorageSets(contextType); 30 | string error = ValidateModels(context, storageSets); 31 | if (error == null) 32 | { 33 | IReadOnlyDictionary metadataMap = await LoadMetadataList(context, storageSets, contextType); 34 | total = await SaveStorageSets(context, total, contextType, storageSets, metadataMap); 35 | _logger.EndGroup(); 36 | } 37 | else 38 | { 39 | _logger.Error("SaveChanges() terminated due to validation error"); 40 | _logger.EndGroup(); 41 | throw new BlazorDBUpdateException(error); 42 | } 43 | 44 | return total; 45 | } 46 | 47 | private string ValidateModels(StorageContext context, IEnumerable storageSets) 48 | { 49 | string error = null; 50 | foreach (PropertyInfo storeageSetProp in storageSets) 51 | { 52 | object storeageSet = storeageSetProp.GetValue(context); 53 | PropertyInfo listProp = storeageSet.GetType().GetProperty(StorageManagerUtil.List, StorageManagerUtil.Flags); 54 | object list = listProp.GetValue(storeageSet); 55 | MethodInfo method = list.GetType().GetMethod(StorageManagerUtil.GetEnumerator); 56 | IEnumerator enumerator = (IEnumerator)method.Invoke(list, new object[] { }); 57 | while (enumerator.MoveNext()) 58 | { 59 | object model = enumerator.Current; 60 | foreach (PropertyInfo prop in model.GetType().GetProperties()) 61 | { 62 | if (Attribute.IsDefined(prop, typeof(Required))) 63 | { 64 | object value = prop.GetValue(model); 65 | if (value == null) 66 | { 67 | error = 68 | $"{model.GetType().FullName}.{prop.Name} is a required field. SaveChanges() has been terminated."; 69 | break; 70 | } 71 | } 72 | 73 | if (Attribute.IsDefined(prop, typeof(MaxLength))) 74 | { 75 | MaxLength maxLength = (MaxLength)Attribute.GetCustomAttribute(prop, typeof(MaxLength)); 76 | object value = prop.GetValue(model); 77 | if (value != null) 78 | { 79 | string str = value.ToString(); 80 | if (str.Length > maxLength.length) 81 | { 82 | error = 83 | $"{model.GetType().FullName}.{prop.Name} length is longer than {maxLength.length}. SaveChanges() has been terminated."; 84 | break; 85 | } 86 | } 87 | } 88 | } 89 | 90 | if (error != null) 91 | { 92 | break; 93 | } 94 | } 95 | } 96 | 97 | return error; 98 | } 99 | 100 | private async Task> LoadMetadataList(StorageContext context, 101 | IEnumerable storageSets, Type contextType) 102 | { 103 | Dictionary map = new Dictionary(); 104 | foreach (PropertyInfo prop in storageSets) 105 | { 106 | Type modelType = prop.PropertyType.GetGenericArguments()[0]; 107 | string storageTableName = Util.GetStorageTableName(contextType, modelType); 108 | Metadata metadata = await _storageManagerUtil.LoadMetadata(storageTableName) ?? new Metadata 109 | { 110 | Guids = new List(), 111 | ContextName = Util.GetFullyQualifiedTypeName(context.GetType()), 112 | ModelName = Util.GetFullyQualifiedTypeName(modelType) 113 | }; 114 | map.Add(Util.GetFullyQualifiedTypeName(modelType), metadata); 115 | } 116 | 117 | return map; 118 | } 119 | 120 | private async Task SaveStorageSets(StorageContext context, int total, Type contextType, 121 | List storageSets, IReadOnlyDictionary metadataMap) 122 | { 123 | foreach (PropertyInfo prop in storageSets) 124 | { 125 | object storageSetValue = prop.GetValue(context); 126 | Type modelType = prop.PropertyType.GetGenericArguments()[0]; 127 | string storageTableName = Util.GetStorageTableName(contextType, modelType); 128 | 129 | EnsureAllModelsHaveIds(storageSetValue, modelType, metadataMap); 130 | EnsureAllAssociationsHaveIds(context, storageSetValue, modelType, storageSets, metadataMap); 131 | 132 | List guids = await SaveModels(storageSetValue, modelType, storageTableName, storageSets); 133 | total += guids.Count; 134 | Metadata oldMetadata = metadataMap[Util.GetFullyQualifiedTypeName(modelType)]; 135 | SaveMetadata(storageTableName, guids, contextType, modelType, oldMetadata); 136 | DeleteOldModelsFromStorage(oldMetadata, storageTableName); 137 | _logger.StorageSetSaved(modelType, guids.Count); 138 | } 139 | 140 | return total; 141 | } 142 | 143 | private void EnsureAllAssociationsHaveIds(StorageContext context, object storageSetValue, Type modelType, 144 | List storageSets, IReadOnlyDictionary metadataMap) 145 | { 146 | Type storageSetType = StorageManagerUtil.GenericStorageSetType.MakeGenericType(modelType); 147 | MethodInfo method = storageSetType.GetMethod(StorageManagerUtil.GetEnumerator); 148 | IEnumerator enumerator = (IEnumerator)method.Invoke(storageSetValue, new object[] { }); 149 | while (enumerator.MoveNext()) 150 | { 151 | object model = enumerator.Current; 152 | foreach (PropertyInfo prop in model.GetType().GetProperties()) 153 | { 154 | if (prop.GetValue(model) == null || !_storageManagerUtil.IsInContext(storageSets, prop) && 155 | !_storageManagerUtil.IsListInContext(storageSets, prop)) 156 | { 157 | continue; 158 | } 159 | 160 | if (_storageManagerUtil.IsInContext(storageSets, prop)) 161 | { 162 | EnsureOneAssociationHasId(context, prop.GetValue(model), prop.PropertyType, storageSets, 163 | metadataMap); 164 | } 165 | 166 | if (_storageManagerUtil.IsListInContext(storageSets, prop)) 167 | { 168 | EnsureManyAssociationHasId(context, prop.GetValue(model), prop, storageSets, metadataMap); 169 | } 170 | } 171 | } 172 | } 173 | 174 | private static void EnsureManyAssociationHasId(StorageContext context, object listObject, PropertyInfo prop, 175 | List storageSets, IReadOnlyDictionary metadataMap) 176 | { 177 | MethodInfo method = listObject.GetType().GetMethod(StorageManagerUtil.GetEnumerator); 178 | IEnumerator enumerator = (IEnumerator)method.Invoke(listObject, new object[] { }); 179 | while (enumerator.MoveNext()) 180 | { 181 | object model = enumerator.Current; 182 | EnsureOneAssociationHasId(context, model, prop.PropertyType.GetGenericArguments()[0], storageSets, 183 | metadataMap); 184 | } 185 | } 186 | 187 | private static void EnsureOneAssociationHasId(StorageContext context, object associatedModel, Type propType, 188 | List storageSets, IReadOnlyDictionary metadataMap) 189 | { 190 | PropertyInfo idProp = GetIdProperty(associatedModel); 191 | string id = Convert.ToString(idProp.GetValue(associatedModel)); 192 | Metadata metadata = metadataMap[Util.GetFullyQualifiedTypeName(propType)]; 193 | if (id == "0") 194 | { 195 | metadata.MaxId = metadata.MaxId + 1; 196 | SaveAssociationModel(context, associatedModel, propType, storageSets, metadata.MaxId); 197 | } 198 | else 199 | { 200 | EnsureAssociationModelExistsOrThrow(context, Convert.ToInt32(id), storageSets, propType); 201 | } 202 | } 203 | 204 | private static void EnsureAssociationModelExistsOrThrow(StorageContext context, int id, 205 | IEnumerable storageSets, Type propType) 206 | { 207 | IEnumerable q = from p in storageSets 208 | where p.PropertyType.GetGenericArguments()[0] == propType 209 | select p; 210 | PropertyInfo storeageSetProp = q.Single(); 211 | object storeageSet = storeageSetProp.GetValue(context); 212 | PropertyInfo listProp = storeageSet.GetType().GetProperty(StorageManagerUtil.List, StorageManagerUtil.Flags); 213 | object list = listProp.GetValue(storeageSet); 214 | MethodInfo method = list.GetType().GetMethod(StorageManagerUtil.GetEnumerator); 215 | IEnumerator enumerator = (IEnumerator)method.Invoke(list, new object[] { }); 216 | bool found = false; 217 | while (enumerator.MoveNext()) 218 | { 219 | object model = enumerator.Current; 220 | if (id != GetId(model)) 221 | { 222 | continue; 223 | } 224 | 225 | found = true; 226 | break; 227 | } 228 | 229 | if (!found) 230 | { 231 | throw new InvalidOperationException( 232 | $"A model of type: {propType.Name} with Id {id} was deleted but still being used by an association. Remove it from the association as well."); 233 | } 234 | } 235 | 236 | private static void EnsureAllModelsHaveIds(object storageSetValue, Type modelType, 237 | IReadOnlyDictionary metadataMap) 238 | { 239 | Type storageSetType = StorageManagerUtil.GenericStorageSetType.MakeGenericType(modelType); 240 | MethodInfo method = storageSetType.GetMethod(StorageManagerUtil.GetEnumerator); 241 | Metadata metadata = metadataMap[Util.GetFullyQualifiedTypeName(modelType)]; 242 | IEnumerator enumerator = (IEnumerator)method.Invoke(storageSetValue, new object[] { }); 243 | while (enumerator.MoveNext()) 244 | { 245 | object model = enumerator.Current; 246 | if (GetId(model) != 0) 247 | { 248 | continue; 249 | } 250 | 251 | metadata.MaxId = metadata.MaxId + 1; 252 | SetId(model, metadata.MaxId); 253 | } 254 | } 255 | 256 | private async void SaveMetadata(string storageTableName, List guids, Type context, Type modelType, 257 | Metadata oldMetadata) 258 | { 259 | Metadata metadata = new Metadata 260 | { 261 | Guids = guids, 262 | ContextName = Util.GetFullyQualifiedTypeName(context), 263 | ModelName = Util.GetFullyQualifiedTypeName(modelType), 264 | MaxId = oldMetadata.MaxId 265 | }; 266 | string name = $"{storageTableName}-{StorageManagerUtil.Metadata}"; 267 | await _blazorDBInterop.SetItem(name, JsonSerializer.Serialize(metadata), false); 268 | } 269 | 270 | private async Task> SaveModels(object storageSetValue, Type modelType, string storageTableName, 271 | List storageSets) 272 | { 273 | List guids = new List(); 274 | Type storageSetType = StorageManagerUtil.GenericStorageSetType.MakeGenericType(modelType); 275 | MethodInfo method = storageSetType.GetMethod(StorageManagerUtil.GetEnumerator); 276 | IEnumerator enumerator = (IEnumerator)method.Invoke(storageSetValue, new object[] { }); 277 | while (enumerator.MoveNext()) 278 | { 279 | Guid guid = Guid.NewGuid(); 280 | guids.Add(guid); 281 | object model = enumerator.Current; 282 | string name = $"{storageTableName}-{guid}"; 283 | string serializedModel = ScanModelForAssociations(model, storageSets, JsonSerializer.Serialize(model)); 284 | await _blazorDBInterop.SetItem(name, serializedModel, false); 285 | } 286 | 287 | return guids; 288 | } 289 | 290 | private void DeleteOldModelsFromStorage(Metadata metadata, string storageTableName) 291 | { 292 | foreach (Guid guid in metadata.Guids) 293 | { 294 | string name = $"{storageTableName}-{guid}"; 295 | _blazorDBInterop.RemoveItem(name, false); 296 | } 297 | } 298 | 299 | private string ScanModelForAssociations(object model, List storageSets, 300 | string serializedModel) 301 | { 302 | string result = serializedModel; 303 | foreach (PropertyInfo prop in model.GetType().GetProperties()) 304 | { 305 | if (prop.GetValue(model) == null || !_storageManagerUtil.IsInContext(storageSets, prop) && 306 | !_storageManagerUtil.IsListInContext(storageSets, prop)) 307 | { 308 | continue; 309 | } 310 | 311 | if (_storageManagerUtil.IsInContext(storageSets, prop)) 312 | { 313 | result = FixOneAssociation(model, prop, result); 314 | } 315 | 316 | if (_storageManagerUtil.IsListInContext(storageSets, prop)) 317 | { 318 | result = FixManyAssociation(model, prop, result); 319 | } 320 | } 321 | 322 | return result; 323 | } 324 | 325 | private static string FixManyAssociation(object model, PropertyInfo prop, string result) 326 | { 327 | IEnumerable modelList = (IEnumerable)prop.GetValue(model); 328 | foreach (object item in modelList) 329 | { 330 | PropertyInfo idProp = GetIdProperty(item); 331 | string id = Convert.ToString(idProp.GetValue(item)); 332 | string serializedItem = JsonSerializer.Serialize(item); 333 | result = ReplaceModelWithId(result, serializedItem, id); 334 | } 335 | 336 | return result; 337 | } 338 | 339 | private static string FixOneAssociation(object model, PropertyInfo prop, string result) 340 | { 341 | object associatedModel = prop.GetValue(model); 342 | PropertyInfo idProp = GetIdProperty(associatedModel); 343 | string id = Convert.ToString(idProp.GetValue(associatedModel)); 344 | string serializedItem = JsonSerializer.Serialize(associatedModel); 345 | result = ReplaceModelWithId(result, serializedItem, id); 346 | return result; 347 | } 348 | 349 | private static int SaveAssociationModel(StorageContext context, object associatedModel, Type propType, 350 | IEnumerable storageSets, int id) 351 | { 352 | IEnumerable q = from p in storageSets 353 | where p.PropertyType.GetGenericArguments()[0] == propType 354 | select p; 355 | PropertyInfo storeageSetProp = q.Single(); 356 | object storeageSet = storeageSetProp.GetValue(context); 357 | PropertyInfo listProp = storeageSet.GetType().GetProperty(StorageManagerUtil.List, StorageManagerUtil.Flags); 358 | object list = listProp.GetValue(storeageSet); 359 | MethodInfo addMethod = list.GetType().GetMethod(StorageManagerUtil.Add); 360 | SetId(associatedModel, id); 361 | addMethod.Invoke(list, new[] { associatedModel }); 362 | return id; 363 | } 364 | 365 | private static string ReplaceModelWithId(string result, string serializedItem, string id) 366 | { 367 | return result.Replace(serializedItem, id); 368 | } 369 | 370 | private static int GetId(object item) 371 | { 372 | PropertyInfo prop = GetIdProperty(item); 373 | return (int)prop.GetValue(item); 374 | } 375 | 376 | private static void SetId(object item, int id) 377 | { 378 | PropertyInfo prop = GetIdProperty(item); 379 | prop.SetValue(item, id); 380 | } 381 | 382 | private static PropertyInfo GetIdProperty(object item) 383 | { 384 | PropertyInfo prop = item.GetType().GetProperty(StorageManagerUtil.Id); 385 | if (prop == null) 386 | { 387 | throw new ArgumentException("Model must have an Id property"); 388 | } 389 | 390 | return prop; 391 | } 392 | } 393 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/StorageManagerUtil.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.Json; 6 | using System.Threading.Tasks; 7 | 8 | namespace BlazorDB.Storage 9 | { 10 | internal class StorageManagerUtil : IStorageManagerUtil 11 | { 12 | public const BindingFlags Flags = BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; 13 | public const string Metadata = "metadata"; 14 | public const string GetEnumerator = "GetEnumerator"; 15 | public const string Id = "Id"; 16 | public const string StorageContextTypeName = "StorageContextTypeName"; 17 | public const string Add = "Add"; 18 | public const string Deserialize = "Deserialize"; 19 | public const string List = "List"; 20 | public static readonly Type GenericStorageSetType = typeof(StorageSet<>); 21 | public static readonly Type GenericListType = typeof(List<>); 22 | public const string JsonId = "id"; 23 | 24 | private readonly IBlazorDBInterop _blazorDBInterop; 25 | 26 | public StorageManagerUtil(IBlazorDBInterop blazorDBInterop) 27 | { 28 | _blazorDBInterop = blazorDBInterop; 29 | } 30 | 31 | public List GetStorageSets(Type contextType) 32 | { 33 | return (from prop in contextType.GetProperties() 34 | where prop.PropertyType.IsGenericType && 35 | prop.PropertyType.GetGenericTypeDefinition() == typeof(StorageSet<>) 36 | select prop).ToList(); 37 | } 38 | 39 | public async Task LoadMetadata(string storageTableName) 40 | { 41 | string name = $"{storageTableName}-{Metadata}"; 42 | string value = await _blazorDBInterop.GetItem(name, false); 43 | return value != null ? JsonSerializer.Deserialize(value) : null; 44 | } 45 | 46 | public string ReplaceString(string source, int start, int end, string stringToInsert) 47 | { 48 | string startStr = source.Substring(0, start); 49 | string endStr = source.Substring(end); 50 | return startStr + stringToInsert + endStr; 51 | } 52 | 53 | public bool IsInContext(List storageSets, PropertyInfo prop) 54 | { 55 | IEnumerable query = from p in storageSets 56 | where p.PropertyType.GetGenericArguments()[0] == prop.PropertyType 57 | select p; 58 | return query.SingleOrDefault() != null; 59 | } 60 | 61 | public bool IsListInContext(List storageSets, PropertyInfo prop) 62 | { 63 | IEnumerable query = from p in storageSets 64 | where prop.PropertyType.IsGenericType && 65 | p.PropertyType.GetGenericArguments()[0] == prop.PropertyType.GetGenericArguments()[0] 66 | select p; 67 | return query.SingleOrDefault() != null; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/BlazorDB/Storage/Util.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BlazorDB 4 | { 5 | internal static class Util 6 | { 7 | internal static string GetStorageTableName(Type Context, Type Model) 8 | { 9 | string databaseName = GetFullyQualifiedTypeName(Context); 10 | string tableName = GetFullyQualifiedTypeName(Model); 11 | return $"{databaseName}-{tableName}"; 12 | } 13 | 14 | internal static string GetFullyQualifiedTypeName(Type type) 15 | { 16 | return $"{type.Namespace}.{type.Name}"; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/BlazorDB/Storage/blazorDBInterop.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace BlazorDB.Storage 6 | { 7 | internal class BlazorDBInterop : IBlazorDBInterop 8 | { 9 | private readonly IJSRuntime _jsRuntime; 10 | 11 | public BlazorDBInterop(IJSRuntime jsRuntime) 12 | { 13 | _jsRuntime = jsRuntime; 14 | } 15 | public Task SetItem(string key, string value, bool session) 16 | { 17 | return _jsRuntime.InvokeAsync("blazorDBInterop.setItem", key, value, session); 18 | } 19 | 20 | public Task GetItem(string key, bool session) 21 | { 22 | return _jsRuntime.InvokeAsync("blazorDBInterop.getItem", key, session); 23 | } 24 | 25 | public Task RemoveItem(string key, bool session) 26 | { 27 | return _jsRuntime.InvokeAsync("blazorDBInterop.removeItem", key, session); 28 | } 29 | 30 | public Task Clear(bool session) 31 | { 32 | return _jsRuntime.InvokeAsync("blazorDBInterop.clear", session); 33 | } 34 | public Task Log(params object[] list) 35 | { 36 | List _list = new List(list); //This line is needed see: https://github.com/aspnet/Blazor/issues/740 37 | return _jsRuntime.InvokeAsync("blazorDBInterop.logs"); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/BlazorDB/StorageContext.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB.Storage; 2 | using System.Threading.Tasks; 3 | 4 | namespace BlazorDB 5 | { 6 | public class StorageContext : IStorageContext 7 | { 8 | protected IStorageManager StorageManager { get; set; } 9 | protected IBlazorDBLogger Logger { get; set; } 10 | protected IStorageManagerUtil StorageManagerUtil { get; set; } 11 | private bool _initalized = false; 12 | 13 | public async Task LogToConsole() 14 | { 15 | await Logger.StartContextType(GetType(), false); 16 | System.Collections.Generic.List storageSets = StorageManagerUtil.GetStorageSets(GetType()); 17 | foreach (System.Reflection.PropertyInfo prop in storageSets) 18 | { 19 | object storageSet = prop.GetValue(this); 20 | System.Reflection.MethodInfo method = storageSet.GetType().GetMethod("LogToConsole"); 21 | method.Invoke(storageSet, new object[] { }); 22 | } 23 | Logger.EndGroup(); 24 | } 25 | 26 | public Task SaveChanges() 27 | { 28 | return StorageManager.SaveContextToLocalStorage(this); 29 | } 30 | 31 | public Task Initialize() 32 | { 33 | if (_initalized) 34 | { 35 | return Task.CompletedTask; 36 | } 37 | 38 | _initalized = true; 39 | return StorageManager.LoadContextFromLocalStorage(this); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/BlazorDB/StorageSet.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB.Storage; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | 6 | namespace BlazorDB 7 | { 8 | public class StorageSet : IList where TModel : class 9 | { 10 | private string StorageContextTypeName { get; set; } 11 | private IBlazorDBLogger Logger { get; set; } 12 | private IList List { get; set; } = new List(); 13 | 14 | public async void LogToConsole() 15 | { 16 | await Logger.LogStorageSetToConsole(GetType(), List); 17 | } 18 | 19 | public TModel this[int index] 20 | { 21 | get => List[index]; 22 | set => List[index] = value; 23 | } 24 | 25 | public int Count => List.Count; 26 | 27 | public bool IsReadOnly => List.IsReadOnly; 28 | 29 | public void Add(TModel item) 30 | { 31 | if (item == null) 32 | { 33 | throw new ArgumentException("Can't add null"); 34 | } 35 | 36 | if (HasId(item)) 37 | { 38 | int id = GetId(item); 39 | if (id > 0) 40 | { 41 | throw new ArgumentException("Can't add item to set that already has an Id", "Id"); 42 | } 43 | 44 | Logger.ItemAddedToContext(StorageContextTypeName, item.GetType(), item); 45 | } 46 | else 47 | { 48 | throw new ArgumentException("Model must have Id property"); 49 | } 50 | 51 | List.Add(item); 52 | } 53 | 54 | public void Clear() 55 | { 56 | List.Clear(); 57 | } 58 | 59 | public bool Contains(TModel item) 60 | { 61 | return List.Contains(item); 62 | } 63 | 64 | public void CopyTo(TModel[] array, int arrayIndex) 65 | { 66 | List.CopyTo(array, arrayIndex); 67 | } 68 | 69 | public IEnumerator GetEnumerator() 70 | { 71 | return List.GetEnumerator(); 72 | } 73 | 74 | public int IndexOf(TModel item) 75 | { 76 | return List.IndexOf(item); 77 | } 78 | 79 | public void Insert(int index, TModel item) 80 | { 81 | List.Insert(index, item); 82 | } 83 | 84 | public bool Remove(TModel item) 85 | { 86 | if (item == null) 87 | { 88 | throw new ArgumentException("Can't remove null"); 89 | } 90 | 91 | bool removed = List.Remove(item); 92 | if (removed) 93 | { 94 | Logger.ItemRemovedFromContext(StorageContextTypeName, item.GetType()); 95 | } 96 | 97 | return removed; 98 | } 99 | 100 | public void RemoveAt(int index) 101 | { 102 | List.RemoveAt(index); 103 | } 104 | 105 | IEnumerator IEnumerable.GetEnumerator() 106 | { 107 | return List.GetEnumerator(); 108 | } 109 | 110 | private static int GetId(TModel item) 111 | { 112 | System.Reflection.PropertyInfo prop = item.GetType().GetProperty("Id"); 113 | if (prop == null) 114 | { 115 | throw new ArgumentException("Model must have an Id property"); 116 | } 117 | 118 | return (int)prop.GetValue(item); 119 | } 120 | 121 | private static bool HasId(TModel item) 122 | { 123 | System.Reflection.PropertyInfo prop = item.GetType().GetProperty("Id"); 124 | return prop != null; 125 | } 126 | } 127 | } -------------------------------------------------------------------------------- /src/BlazorDB/wwwroot/blazorDBInterop.js: -------------------------------------------------------------------------------- 1 | window.blazorDBInterop = { 2 | setItem: function(key, value, session) { 3 | if (session) { 4 | sessionStorage.setItem(key, value); 5 | } else { 6 | localStorage.setItem(key, value); 7 | } 8 | return true; 9 | }, 10 | getItem: function (key, session) { 11 | if(session) { 12 | return sessionStorage.getItem(key); 13 | } else { 14 | return localStorage.getItem(key); 15 | } 16 | }, 17 | removeItem: function (key, session) { 18 | if(session) { 19 | sessionStorage.removeItem(key); 20 | } else { 21 | localStorage.removeItem(key); 22 | } 23 | return true; 24 | }, 25 | clear: function (session) { 26 | if(session) { 27 | sessionStorage.clear(); 28 | } else { 29 | localStorage.clear(); 30 | } 31 | return true; 32 | } 33 | }; -------------------------------------------------------------------------------- /src/Sample/App.razor: -------------------------------------------------------------------------------- 1 |  5 | 6 | -------------------------------------------------------------------------------- /src/Sample/Models/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Models 2 | { 3 | public class Address 4 | { 5 | public int Id { get; set; } 6 | public string Street { get; set; } 7 | public string City { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Sample/Models/AssociationContext.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB; 2 | 3 | namespace Sample.Models 4 | { 5 | public class AssociationContext : StorageContext 6 | { 7 | public StorageSet People { get; set; } 8 | public StorageSet
Addresses { get; set; } 9 | public StorageSet States { get; set; } 10 | } 11 | } -------------------------------------------------------------------------------- /src/Sample/Models/Context.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB; 2 | 3 | namespace Sample.Models 4 | { 5 | public class Context : StorageContext 6 | { 7 | public StorageSet People { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Sample/Models/Person.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB.DataAnnotations; 2 | using System.Collections.Generic; 3 | 4 | namespace Sample.Models 5 | { 6 | public class Person 7 | { 8 | public int Id { get; set; } 9 | [Required] 10 | public string FirstName { get; set; } 11 | [MaxLength(20)] 12 | public string LastName { get; set; } 13 | public Address HomeAddress { get; set; } 14 | public List
OtherAddresses { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Sample/Models/State.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Models 2 | { 3 | public class State 4 | { 5 | public int Id { get; set; } 6 | public string Abbr { get; set; } 7 | public string Name { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Sample/Models/TodoContext.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB; 2 | 3 | namespace Sample.Models 4 | { 5 | public class TodoContext : StorageContext 6 | { 7 | public StorageSet Todos { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Sample/Models/TodoItem.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.Models 2 | { 3 | public class TodoItem 4 | { 5 | public int Id { get; set; } 6 | public string Text { get; set; } 7 | public bool IsDone { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /src/Sample/Pages/Associations.razor: -------------------------------------------------------------------------------- 1 | @using Sample.Models 2 | @page "/associations" 3 | @inject AssociationContext Context 4 | 5 | 6 |

Associations

7 | 8 |

One to One

9 | 10 | Add a Person and Address 11 | 12 | Change Address 13 | 14 |

One to Many

15 | 16 | Add a Person with multi Addresses 17 | 18 | Load Person 19 | 20 |

Delete Test (Should throw exception)

21 | 22 | Add a Person with Addresses then delete Address 23 | 24 |

Result

25 | 26 | @if (_person != null) 27 | { 28 |
29 | @_person.FirstName 30 |
31 |
Addresses:
32 | 33 | @foreach (var address in _person.OtherAddresses) 34 | { 35 | 36 | @address.Id 37 | 38 | @address.Street 39 | 40 | 41 | } 42 | 43 | } 44 | 45 | @functions { 46 | Person _person; 47 | protected async override Task OnInitAsync() 48 | { 49 | await Context.Initialize(); 50 | } 51 | void OnAdd(UIMouseEventArgs e) 52 | { 53 | var person = new Person { FirstName = "John", LastName = "Smith" }; 54 | var address = new Address { Street = "221 Baker Streeet", City = "This should be a refrence to address since Address exists in the context" }; 55 | person.HomeAddress = address; 56 | Context.People.Add(person); 57 | Context.SaveChanges(); 58 | StateHasChanged(); 59 | } 60 | 61 | void OnChange(UIMouseEventArgs e) 62 | { 63 | Context.People[0].HomeAddress.Street = "Changed Streeet"; 64 | Context.SaveChanges(); 65 | Console.WriteLine("Person address changed: {0}", Context.People[0].HomeAddress.Street); 66 | Console.WriteLine("Address entity changed as well: {0}", Context.Addresses[0].Street); 67 | StateHasChanged(); 68 | } 69 | 70 | void OnAddMulti(UIMouseEventArgs e) 71 | { 72 | var person = new Person { FirstName = "Many", LastName = "Test" }; 73 | var address1 = new Address { Street = "Many test 1", City = "Saved as a reference" }; 74 | var address2 = new Address { Street = "Many test 2", City = "Saved as a reference" }; 75 | person.OtherAddresses = new List
{ address1, address2 }; 76 | Context.People.Add(person); 77 | Context.SaveChanges(); 78 | StateHasChanged(); 79 | } 80 | 81 | void OnLoadPerson(UIMouseEventArgs e) 82 | { 83 | _person = Context.People[1]; 84 | StateHasChanged(); 85 | } 86 | 87 | void OnDelete(UIMouseEventArgs e) 88 | { 89 | var person = new Person { FirstName = "John", LastName = "Smith" }; 90 | var address = new Address { Street = "221 Baker Streeet", City = "This should be a refrence to address since Address exists in the context" }; 91 | person.HomeAddress = address; 92 | Context.People.Add(person); 93 | Context.SaveChanges(); 94 | 95 | Context.Addresses.RemoveAt(0); 96 | Context.SaveChanges(); 97 | } 98 | } -------------------------------------------------------------------------------- /src/Sample/Pages/Index.razor: -------------------------------------------------------------------------------- 1 | @using Sample.Models 2 | @page "/" 3 | @inject Context Context 4 | 5 |

BlazorDB Demo Site

6 | 7 | Add Person 8 | 9 | Remove Person 10 | 11 | Get Person 12 | 13 | SaveChanges 14 | 15 |

Validations

16 | 17 | Required 18 | 19 | Max Length 20 | 21 | @if (@Person != null) 22 | { 23 |
24 | Person: @Person.FirstName @Person.LastName 25 |
26 | } 27 | 28 | @functions { 29 | private Person Person { get; set; } 30 | protected async override Task OnInitAsync() 31 | { 32 | await Context.Initialize(); 33 | } 34 | void OnclickAddPerson() 35 | { 36 | //In this case, address gets serailized as part of the object because Address is not in the Context 37 | var person = new Person { FirstName = "John", LastName = "Smith", HomeAddress = new Address { Street = "221 Baker Street", City = "This should be part of the json, since address is not in context (Very long city)" } }; 38 | var address1 = new Address { Street = "Many test 1", City = "Saved as a reference" }; 39 | var address2 = new Address { Street = "Many test 2", City = "Saved as a reference" }; 40 | person.OtherAddresses = new List
{ address1, address2 }; 41 | Context.People.Add(person); 42 | } 43 | 44 | void onclickRemovePerson() 45 | { 46 | var query = from person in Context.People 47 | where person.Id == 1 48 | select person; 49 | var personToRemove = query.Single(); 50 | Context.People.Remove(personToRemove); 51 | } 52 | 53 | void OnclickGetPerson() 54 | { 55 | var query = from person in Context.People 56 | where person.Id == 1 57 | select person; 58 | Person = query.Single(); 59 | StateHasChanged(); 60 | } 61 | 62 | async void OnclickSaveChanges() 63 | { 64 | await Context.SaveChanges(); 65 | } 66 | 67 | void OnRequired() 68 | { 69 | var person = new Person { LastName = "Firstname required!" }; 70 | Context.People.Add(person); 71 | Context.SaveChanges(); 72 | } 73 | 74 | void OnMaxLength() 75 | { 76 | var person = new Person { FirstName = "Lastname is long", LastName = "This is a very long last name and should throw!" }; 77 | Context.People.Add(person); 78 | Context.SaveChanges(); 79 | } 80 | } -------------------------------------------------------------------------------- /src/Sample/Pages/TodoItemForm.razor: -------------------------------------------------------------------------------- 1 | @using Sample.Models 2 | @inject TodoContext Context 3 | 4 | 5 | 6 | Id 7 | 8 | 9 | 10 | 11 | 12 | Text 13 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 |
22 |
23 | 24 | Submit 25 | 26 |
27 | 28 | @functions { 29 | [Parameter] int SelectedId { get; set; } 30 | [Parameter] Action NewTodoAdded { get; set; } 31 | 32 | TodoItem Todo { get; set; } = new TodoItem(); 33 | 34 | protected async override Task OnInitAsync() 35 | { 36 | if (Context.Todos == null) 37 | { 38 | await Context.Initialize(); 39 | } 40 | if (SelectedId == 0) 41 | { 42 | Todo = new TodoItem(); 43 | } 44 | else 45 | { 46 | SetTodo(); 47 | } 48 | } 49 | 50 | protected async override Task OnParametersSetAsync() 51 | { 52 | if (Context.Todos == null) 53 | { 54 | await Context.Initialize(); 55 | } 56 | SetTodo(); 57 | } 58 | 59 | void SetTodo() 60 | { 61 | if (Context.Todos == null) 62 | { 63 | return; 64 | } 65 | var query = from todo in Context.Todos 66 | where todo.Id == SelectedId 67 | select todo; 68 | var item = query.FirstOrDefault(); 69 | Todo = item != null ? item : new TodoItem(); 70 | } 71 | 72 | async void onclick() 73 | { 74 | if (Todo.Id == 0) 75 | { 76 | Context.Todos.Add(Todo); //add new todo to the context 77 | } 78 | await Context.SaveChanges(); 79 | StateHasChanged(); 80 | NewTodoAdded(); 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /src/Sample/Pages/Todos.razor: -------------------------------------------------------------------------------- 1 | @page "/todos" 2 | @inject TodoContext Context 3 | 4 |

Todos

5 | 6 | 7 | 8 | Filter 9 | 10 | 11 | 12 | 13 | IdIs DoneText 14 | 15 | 16 | @foreach (var todo in Filtered) 17 | { 18 | var color = todo.IsDone ? Color.Success : Color.Danger; 19 | 20 | @todo.Id 21 | @todo.IsDone 22 | @todo.Text 23 | 24 | } 25 | 26 | 27 | Add New 28 |

Selected Item

29 | 30 | 31 | @functions { 32 | string Filter { get; set; } 33 | List Filtered { get; set; } = new List(); 34 | int SelectedId { get; set; } = 1; 35 | 36 | protected async override Task OnInitAsync() 37 | { 38 | await Context.Initialize(); 39 | if (Context.Todos.Count == 0) 40 | { 41 | //Insert some initial data 42 | Context.Todos.Add(new TodoItem { IsDone = true, Text = "Create initial BlazorDB project" }); 43 | Context.Todos.Add(new TodoItem { IsDone = true, Text = "Make a todo app" }); 44 | Context.Todos.Add(new TodoItem { IsDone = false, Text = "Make BlazorDB really awesome!" }); 45 | await Context.SaveChanges(); 46 | } 47 | Filtered = Context.Todos.ToList(); 48 | } 49 | 50 | void onFilter() 51 | { 52 | if(Filter == String.Empty) 53 | { 54 | Filtered = Context.Todos.ToList(); 55 | } 56 | else 57 | { 58 | var query = from todo in Context.Todos 59 | where todo.Text.ToLower().Contains(Filter.ToLower()) 60 | select todo; 61 | Filtered = query.ToList(); 62 | } 63 | } 64 | 65 | void onClickItem(int i) 66 | { 67 | SelectedId = i; 68 | StateHasChanged(); 69 | } 70 | 71 | void addNewClick() 72 | { 73 | SelectedId = 0; 74 | StateHasChanged(); 75 | } 76 | 77 | void NewTodoAdded() 78 | { 79 | StateHasChanged(); 80 | } 81 | } -------------------------------------------------------------------------------- /src/Sample/Pages/_Imports.razor: -------------------------------------------------------------------------------- 1 | @layout MainLayout 2 | @using Sample.Models 3 | -------------------------------------------------------------------------------- /src/Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Blazor.Hosting; 2 | 3 | namespace Sample 4 | { 5 | public class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | CreateHostBuilder(args).Build().Run(); 10 | } 11 | 12 | public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) 13 | { 14 | return BlazorWebAssemblyHost.CreateDefaultBuilder() 15 | .UseBlazorStartup(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Sample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:63096/", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "Sample": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "environmentVariables": { 22 | "ASPNETCORE_ENVIRONMENT": "Development" 23 | }, 24 | "applicationUrl": "http://localhost:63096/" 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Sample/Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | 6 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; 7 | https://dotnet.myget.org/F/blazor-dev/api/v3/index.json; 8 | 9 | 7.3 10 | 3.0 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Sample/Shared/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @inject IBootstrapCSS BootstrapCSS 3 | 4 | 7 | 8 |
9 |
10 | @Body 11 |
12 |
13 | 14 | @code { 15 | protected override async Task OnInitAsync() 16 | { 17 | await BootstrapCSS.SetBootstrapCSS("united", "4.3.1"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Sample/Shared/NavMenu.razor: -------------------------------------------------------------------------------- 1 |  7 | 8 |
9 | 22 |
23 | 24 | @functions { 25 | bool collapseNavMenu = true; 26 | 27 | void ToggleNavMenu() 28 | { 29 | collapseNavMenu = !collapseNavMenu; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Sample/Startup.cs: -------------------------------------------------------------------------------- 1 | using BlazorDB; 2 | using BlazorStrap; 3 | using Microsoft.AspNetCore.Components.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | 7 | namespace Sample 8 | { 9 | public class Startup 10 | { 11 | public void ConfigureServices(IServiceCollection services) 12 | { 13 | services.AddBlazorDB(options => 14 | { 15 | options.LogDebug = true; 16 | options.Assembly = typeof(Program).Assembly; 17 | }); 18 | services.AddBootstrapCSS(); 19 | } 20 | 21 | public void Configure(IComponentsApplicationBuilder app) 22 | { 23 | app.AddComponent("app"); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Sample/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using Microsoft.AspNetCore.Components.Layouts 3 | @using Microsoft.AspNetCore.Components.Routing 4 | @using Microsoft.JSInterop 5 | @using Sample 6 | @using Sample.Shared 7 | @using BlazorStrap 8 | -------------------------------------------------------------------------------- /src/Sample/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | app { 6 | position: relative; 7 | display: flex; 8 | flex-direction: column; 9 | } 10 | 11 | .top-row { 12 | height: 3.5rem; 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | .main { 18 | flex: 1; 19 | } 20 | 21 | .main .top-row { 22 | background-color: #e6e6e6; 23 | border-bottom: 1px solid #d6d5d5; 24 | } 25 | 26 | .sidebar { 27 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 28 | } 29 | 30 | .sidebar .top-row { 31 | background-color: rgba(0,0,0,0.4); 32 | } 33 | 34 | .sidebar .navbar-brand { 35 | font-size: 1.1rem; 36 | } 37 | 38 | .sidebar .oi { 39 | width: 2rem; 40 | font-size: 1.1rem; 41 | vertical-align: text-top; 42 | top: -2px; 43 | } 44 | 45 | .nav-item { 46 | font-size: 0.9rem; 47 | padding-bottom: 0.5rem; 48 | } 49 | 50 | .nav-item:first-of-type { 51 | padding-top: 1rem; 52 | } 53 | 54 | .nav-item:last-of-type { 55 | padding-bottom: 1rem; 56 | } 57 | 58 | .nav-item a { 59 | color: #d7d7d7; 60 | border-radius: 4px; 61 | height: 3rem; 62 | display: flex; 63 | align-items: center; 64 | line-height: 3rem; 65 | } 66 | 67 | .nav-item a.active { 68 | background-color: rgba(255,255,255,0.25); 69 | color: white; 70 | } 71 | 72 | .nav-item a:hover { 73 | background-color: rgba(255,255,255,0.1); 74 | color: white; 75 | } 76 | 77 | .content { 78 | padding-top: 1.1rem; 79 | } 80 | 81 | .navbar-toggler { 82 | background-color: rgba(255, 255, 255, 0.1); 83 | } 84 | 85 | .valid.modified:not([type=checkbox]) { 86 | outline: 1px solid #26b050; 87 | } 88 | 89 | .invalid { 90 | outline: 1px solid red; 91 | } 92 | 93 | .validation-message { 94 | color: red; 95 | } 96 | 97 | @media (max-width: 767.98px) { 98 | .main .top-row { 99 | display: none; 100 | } 101 | } 102 | 103 | @media (min-width: 768px) { 104 | app { 105 | flex-direction: row; 106 | } 107 | 108 | .sidebar { 109 | width: 250px; 110 | height: 100vh; 111 | position: sticky; 112 | top: 0; 113 | } 114 | 115 | .main .top-row { 116 | position: sticky; 117 | top: 0; 118 | } 119 | 120 | .main > div { 121 | padding-left: 2rem !important; 122 | padding-right: 1.5rem !important; 123 | } 124 | 125 | .navbar-toggler { 126 | display: none; 127 | } 128 | 129 | .sidebar .collapse { 130 | /* Never collapse the sidebar for wide screens */ 131 | display: block; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/Sample/wwwroot/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | BlazorDB Demo Site 7 | 8 | 9 | 10 | 11 | Loading... 12 | 13 | 14 | --------------------------------------------------------------------------------