Browse Source

InputFile follow-up (#25248)

* Changes from API review

* Feedback from security review

* Update IBrowserFile.cs

* Updated documentation.

* Moved InputFile to M.A.Components.Forms

* More changes

* Move ProtectedBrowserStorage to it's own package
* Mark Components.Web.Extensions as non-shipping until we can move it over
* Allow InputFile.OpenReadStreamAsync to specify an expected file size.

* Fix E2E tests

* CR: Throw if user supplies too many files.

* Another build fix

* CR: Zero files is not a scenario

* Update E2E tests

* Update JS binaries after rebase

* Update test

Co-authored-by: Pranav K <[email protected]>
Co-authored-by: Steve Sanderson <[email protected]>
Mackinnon Buck 5 years ago
parent
commit
c2f97933fe
48 changed files with 719 additions and 385 deletions
  1. 33 15
      AspNetCore.sln
  2. 1 0
      eng/ProjectReferences.props
  3. 3 2
      src/Components/Components.slnf
  4. 1 1
      src/Components/Components/src/Properties/AssemblyInfo.cs
  5. 3 2
      src/Components/ComponentsNoDeps.slnf
  6. 20 0
      src/Components/ProtectedBrowserStorage/src/Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj
  7. 4 0
      src/Components/ProtectedBrowserStorage/src/Properties/AssemblyInfo.cs
  8. 1 1
      src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs
  9. 4 1
      src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedBrowserStorageResult.cs
  10. 1 1
      src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedBrowserStorageServiceCollectionExtensions.cs
  11. 1 1
      src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs
  12. 1 1
      src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs
  13. 1 2
      src/Components/ProtectedBrowserStorage/test/Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj
  14. 1 1
      src/Components/ProtectedBrowserStorage/test/ProtectedBrowserStorageTest.cs
  15. 0 33
      src/Components/Web.Extensions/src/InputFile/BrowserFile.cs
  16. 0 54
      src/Components/Web.Extensions/src/InputFile/IBrowserFile.cs
  17. 0 28
      src/Components/Web.Extensions/src/InputFile/InputFileChangeEventArgs.cs
  18. 1 1
      src/Components/Web.Extensions/src/Microsoft.AspNetCore.Components.Web.Extensions.csproj
  19. 0 142
      src/Components/Web.Extensions/src/wwwroot/inputFile.js
  20. 0 0
      src/Components/Web.JS/dist/Release/blazor.server.js
  21. 0 0
      src/Components/Web.JS/dist/Release/blazor.webassembly.js
  22. 2 0
      src/Components/Web.JS/src/GlobalExports.ts
  23. 159 0
      src/Components/Web.JS/src/InputFile.ts
  24. 3 3
      src/Components/Web/src/Forms/InputFile.cs
  25. 50 0
      src/Components/Web/src/Forms/InputFile/BrowserFile.cs
  26. 40 0
      src/Components/Web/src/Forms/InputFile/BrowserFileExtensions.cs
  27. 1 1
      src/Components/Web/src/Forms/InputFile/BrowserFileStream.cs
  28. 62 0
      src/Components/Web/src/Forms/InputFile/IBrowserFile.cs
  29. 1 1
      src/Components/Web/src/Forms/InputFile/IInputFileJsCallbacks.cs
  30. 57 0
      src/Components/Web/src/Forms/InputFile/InputFileChangeEventArgs.cs
  31. 2 2
      src/Components/Web/src/Forms/InputFile/InputFileInterop.cs
  32. 1 1
      src/Components/Web/src/Forms/InputFile/InputFileJsCallbacksRelay.cs
  33. 1 1
      src/Components/Web/src/Forms/InputFile/ReadRequest.cs
  34. 2 2
      src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs
  35. 3 3
      src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStreamOptions.cs
  36. 1 1
      src/Components/Web/src/Forms/InputFile/SharedBrowserFileStream.cs
  37. 33 0
      src/Components/Web/test/Forms/BrowserFileTest.cs
  38. 69 0
      src/Components/Web/test/Forms/InputFileChangeEventArgsTest.cs
  39. 8 0
      src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs
  40. 41 1
      src/Components/test/E2ETest/Tests/InputFileTest.cs
  41. 1 0
      src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
  42. 102 0
      src/Components/test/testassets/BasicTestApp/FormsTest/InputFileComponent.razor
  43. 1 1
      src/Components/test/testassets/BasicTestApp/Index.razor
  44. 0 80
      src/Components/test/testassets/BasicTestApp/InputFileComponent.razor
  45. 1 0
      src/Components/test/testassets/BasicTestApp/Program.cs
  46. 1 0
      src/Components/test/testassets/BasicTestApp/ProtectedBrowserStorageInjectionComponent.razor
  47. 1 0
      src/Components/test/testassets/BasicTestApp/ProtectedBrowserStorageUsageComponent.razor
  48. 0 2
      src/Components/test/testassets/BasicTestApp/wwwroot/index.html

+ 33 - 15
AspNetCore.sln

@@ -1395,8 +1395,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web.Extensions", "Web.Exten
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions", "src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj", "{8294A74F-7DAA-4B69-BC56-7634D93C9693}"
 EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.Web.Extensions.Tests", "src\Components\Web.Extensions\test\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj", "{157605CB-5170-4C1A-980F-4BAE42DB60DE}"
-EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sdk", "Sdk", "{FED4267E-E5E4-49C5-98DB-8B3F203596EE}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.NET.Sdk.BlazorWebAssembly", "src\Components\WebAssembly\Sdk\src\Microsoft.NET.Sdk.BlazorWebAssembly.csproj", "{6B2734BF-C61D-4889-ABBF-456A4075D59B}"
@@ -1489,6 +1487,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagno
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.JSInterop.Tests", "src\JSInterop\Microsoft.JSInterop\test\Microsoft.JSInterop.Tests.csproj", "{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}"
 EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ProtectedBrowserStorage", "ProtectedBrowserStorage", "{1B06FD32-3A1D-46A4-B2AF-99159FAD8127}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.ProtectedBrowserStorage", "src\Components\ProtectedBrowserStorage\src\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj", "{9059AC97-7547-4CC1-A076-680CBCCC1F33}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests", "src\Components\ProtectedBrowserStorage\test\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj", "{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -6671,18 +6675,6 @@ Global
 		{8294A74F-7DAA-4B69-BC56-7634D93C9693}.Release|x64.Build.0 = Release|Any CPU
 		{8294A74F-7DAA-4B69-BC56-7634D93C9693}.Release|x86.ActiveCfg = Release|Any CPU
 		{8294A74F-7DAA-4B69-BC56-7634D93C9693}.Release|x86.Build.0 = Release|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x64.ActiveCfg = Debug|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x64.Build.0 = Debug|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x86.ActiveCfg = Debug|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Debug|x86.Build.0 = Debug|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|Any CPU.Build.0 = Release|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.ActiveCfg = Release|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x64.Build.0 = Release|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.ActiveCfg = Release|Any CPU
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE}.Release|x86.Build.0 = Release|Any CPU
 		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{6B2734BF-C61D-4889-ABBF-456A4075D59B}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -7113,6 +7105,30 @@ Global
 		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x64.Build.0 = Release|Any CPU
 		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.ActiveCfg = Release|Any CPU
 		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E}.Release|x86.Build.0 = Release|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x64.Build.0 = Debug|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Debug|x86.Build.0 = Debug|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|Any CPU.Build.0 = Release|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x64.ActiveCfg = Release|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x64.Build.0 = Release|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x86.ActiveCfg = Release|Any CPU
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33}.Release|x86.Build.0 = Release|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x64.ActiveCfg = Debug|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x64.Build.0 = Debug|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x86.ActiveCfg = Debug|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Debug|x86.Build.0 = Debug|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|Any CPU.Build.0 = Release|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x64.ActiveCfg = Release|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x64.Build.0 = Release|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x86.ActiveCfg = Release|Any CPU
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5}.Release|x86.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -7814,7 +7830,6 @@ Global
 		{1542DC58-1836-4191-A9C5-51D1716D2543} = {05A169C7-4F20-4516-B10A-B13C5649D346}
 		{F71FE795-9923-461B-9809-BB1821A276D0} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
 		{8294A74F-7DAA-4B69-BC56-7634D93C9693} = {F71FE795-9923-461B-9809-BB1821A276D0}
-		{157605CB-5170-4C1A-980F-4BAE42DB60DE} = {F71FE795-9923-461B-9809-BB1821A276D0}
 		{FED4267E-E5E4-49C5-98DB-8B3F203596EE} = {562D5067-8CD8-4F19-BCBB-873204932C61}
 		{6B2734BF-C61D-4889-ABBF-456A4075D59B} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
 		{83371889-9A3E-4D16-AE77-EB4F83BC6374} = {FED4267E-E5E4-49C5-98DB-8B3F203596EE}
@@ -7861,6 +7876,9 @@ Global
 		{55CACC1F-FE96-47C8-8073-91F4CAA55C75} = {2A91479A-4ABE-4BB7-9A5E-CA3B9CCFC69E}
 		{7509AA1E-3093-4BEE-984F-E11579E98A11} = {7CB09412-C9B0-47E8-A8C3-311AA4CFDE04}
 		{DAAB6B35-CBD2-4573-B633-CDD42F583A0E} = {16898702-3E33-41C1-B8D8-4CE3F1D46BD9}
+		{1B06FD32-3A1D-46A4-B2AF-99159FAD8127} = {60D51C98-2CC0-40DF-B338-44154EFEE2FF}
+		{9059AC97-7547-4CC1-A076-680CBCCC1F33} = {1B06FD32-3A1D-46A4-B2AF-99159FAD8127}
+		{943FD3EC-D330-4277-B3F3-3DFABB57D3B5} = {1B06FD32-3A1D-46A4-B2AF-99159FAD8127}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}

+ 1 - 0
eng/ProjectReferences.props

@@ -140,6 +140,7 @@
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components" ProjectPath="$(RepoRoot)src\Components\Components\src\Microsoft.AspNetCore.Components.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Forms" ProjectPath="$(RepoRoot)src\Components\Forms\src\Microsoft.AspNetCore.Components.Forms.csproj" />
     <ProjectReferenceProvider Include="Ignitor" ProjectPath="$(RepoRoot)src\Components\Ignitor\src\Ignitor.csproj" />
+    <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage" ProjectPath="$(RepoRoot)src\Components\ProtectedBrowserStorage\src\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Server" ProjectPath="$(RepoRoot)src\Components\Server\src\Microsoft.AspNetCore.Components.Server.csproj" />
     <ProjectReferenceProvider Include="Microsoft.AspNetCore.Components.Web.Extensions" ProjectPath="$(RepoRoot)src\Components\Web.Extensions\src\Microsoft.AspNetCore.Components.Web.Extensions.csproj" />
     <ProjectReferenceProvider Include="Microsoft.Authentication.WebAssembly.Msal" ProjectPath="$(RepoRoot)src\Components\WebAssembly\Authentication.Msal\src\Microsoft.Authentication.WebAssembly.Msal.csproj" />

+ 3 - 2
src/Components/Components.slnf

@@ -101,7 +101,6 @@
       "src\\Components\\benchmarkapps\\Wasm.Performance\\Driver\\Wasm.Performance.Driver.csproj",
       "src\\Components\\benchmarkapps\\Wasm.Performance\\TestApp\\Wasm.Performance.TestApp.csproj",
       "src\\Components\\Web.Extensions\\src\\Microsoft.AspNetCore.Components.Web.Extensions.csproj",
-      "src\\Components\\Web.Extensions\\test\\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj",
       "src\\Components\\WebAssembly\\Server\\test\\Microsoft.AspNetCore.Components.WebAssembly.Server.Tests.csproj",
       "src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj",
       "src\\Components\\WebAssembly\\JSInterop\\src\\Microsoft.JSInterop.WebAssembly.csproj",
@@ -112,7 +111,9 @@
       "src\\Components\\WebAssembly\\Sdk\\src\\Microsoft.NET.Sdk.BlazorWebAssembly.csproj",
       "src\\Components\\WebAssembly\\Sdk\\test\\Microsoft.NET.Sdk.BlazorWebAssembly.Tests.csproj",
       "src\\Components\\WebAssembly\\Sdk\\integrationtests\\Microsoft.NET.Sdk.BlazorWebAssembly.IntegrationTests.csproj",
-      "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj"
+      "src\\JSInterop\\Microsoft.JSInterop\\src\\Microsoft.JSInterop.csproj",
+      "src\\Components\\ProtectedBrowserStorage\\src\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj",
+      "src\\Components\\ProtectedBrowserStorage\\test\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj"
     ]
   }
 }

+ 1 - 1
src/Components/Components/src/Properties/AssemblyInfo.cs

@@ -7,6 +7,6 @@ using System.Runtime.CompilerServices;
 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Server.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
 [assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
-[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.Web.Extensions.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
 
 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")]

+ 3 - 2
src/Components/ComponentsNoDeps.slnf

@@ -17,7 +17,6 @@
       "src\\Components\\Server\\src\\Microsoft.AspNetCore.Components.Server.csproj",
       "src\\Components\\Server\\test\\Microsoft.AspNetCore.Components.Server.Tests.csproj",
       "src\\Components\\Web.Extensions\\src\\Microsoft.AspNetCore.Components.Web.Extensions.csproj",
-      "src\\Components\\Web.Extensions\\test\\Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj",
       "src\\Components\\WebAssembly\\Authentication.Msal\\src\\Microsoft.Authentication.WebAssembly.Msal.csproj",
       "src\\Components\\WebAssembly\\DevServer\\src\\Microsoft.AspNetCore.Components.WebAssembly.DevServer.csproj",
       "src\\Components\\WebAssembly\\JSInterop\\src\\Microsoft.JSInterop.WebAssembly.csproj",
@@ -46,7 +45,9 @@
       "src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
       "src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
       "src\\Components\\test\\testassets\\TestContentPackage\\TestContentPackage.csproj",
-      "src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj"
+      "src\\Components\\test\\testassets\\TestServer\\Components.TestServer.csproj",
+      "src\\Components\\ProtectedBrowserStorage\\src\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj",
+      "src\\Components\\ProtectedBrowserStorage\\test\\Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj"
     ]
   }
 }

+ 20 - 0
src/Components/ProtectedBrowserStorage/src/Microsoft.AspNetCore.Components.ProtectedBrowserStorage.csproj

@@ -0,0 +1,20 @@
+<Project Sdk="Microsoft.NET.Sdk.Razor">
+
+  <PropertyGroup>
+    <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
+    <Description>Provides functionality for storing protected data using the browser's localStorage and sessionStorage APIs.</Description>
+    <GenerateDocumentationFile>true</GenerateDocumentationFile>
+    <Nullable>enable</Nullable>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Reference Include="Microsoft.AspNetCore.Components" />
+    <Reference Include="Microsoft.AspNetCore.DataProtection" />
+    <Reference Include="Microsoft.JSInterop" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Compile Include="..\..\Shared\src\JsonSerializerOptionsProvider.cs" />
+  </ItemGroup>
+
+</Project>

+ 4 - 0
src/Components/ProtectedBrowserStorage/src/Properties/AssemblyInfo.cs

@@ -0,0 +1,4 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.Versioning;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]

+ 1 - 1
src/Components/Web.Extensions/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs → src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedBrowserStorage.cs

@@ -10,7 +10,7 @@ using System.Threading.Tasks;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 {
 
     /// <summary>

+ 4 - 1
src/Components/Web.Extensions/src/ProtectedBrowserStorage/ProtectedBrowserStorageResult.cs → src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedBrowserStorageResult.cs

@@ -1,6 +1,9 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
 using System.Diagnostics.CodeAnalysis;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 {
     /// <summary>
     /// Contains the result of a protected browser storage operation.

+ 1 - 1
src/Components/Web.Extensions/src/ProtectedBrowserStorage/ProtectedBrowserStorageServiceCollectionExtensions.cs → src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedBrowserStorageServiceCollectionExtensions.cs

@@ -1,7 +1,7 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-using Microsoft.AspNetCore.Components.Web.Extensions;
+using Microsoft.AspNetCore.Components.ProtectedBrowserStorage;
 
 namespace Microsoft.Extensions.DependencyInjection
 {

+ 1 - 1
src/Components/Web.Extensions/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs → src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedLocalStorage.cs

@@ -5,7 +5,7 @@ using System.Runtime.Versioning;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 {
     /// <summary>
     /// Provides mechanisms for storing and retrieving data in the browser's

+ 1 - 1
src/Components/Web.Extensions/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs → src/Components/ProtectedBrowserStorage/src/ProtectedBrowserStorage/ProtectedSessionStorage.cs

@@ -5,7 +5,7 @@ using System.Runtime.Versioning;
 using Microsoft.AspNetCore.DataProtection;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 {
     /// <summary>
     /// Provides mechanisms for storing and retrieving data in the browser's

+ 1 - 2
src/Components/Web.Extensions/test/Microsoft.AspNetCore.Components.Web.Extensions.Tests.csproj → src/Components/ProtectedBrowserStorage/test/Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests.csproj

@@ -2,12 +2,11 @@
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
-    <RootNamespace>Microsoft.AspNetCore.Components</RootNamespace>
   </PropertyGroup>
 
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.Components" />
-    <Reference Include="Microsoft.AspNetCore.Components.Web.Extensions" />
+    <Reference Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage" />
     <Reference Include="Microsoft.AspNetCore.WebUtilities" />
     <Reference Include="Microsoft.Extensions.DependencyInjection" />
   </ItemGroup>

+ 1 - 1
src/Components/Web.Extensions/test/ProtectedBrowserStorageTest.cs → src/Components/ProtectedBrowserStorage/test/ProtectedBrowserStorageTest.cs

@@ -14,7 +14,7 @@ using Microsoft.AspNetCore.WebUtilities;
 using Microsoft.JSInterop;
 using Xunit;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 {
     public class ProtectedBrowserStorageTest
     {

+ 0 - 33
src/Components/Web.Extensions/src/InputFile/BrowserFile.cs

@@ -1,33 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.AspNetCore.Components.Web.Extensions
-{
-    internal class BrowserFile : IBrowserFile
-    {
-        internal InputFile Owner { get; set; } = default!;
-
-        public int Id { get; set; }
-
-        public string Name { get; set; } = string.Empty;
-
-        public DateTime LastModified { get; set; }
-
-        public long Size { get; set; }
-
-        public string Type { get; set; } = string.Empty;
-
-        public string? RelativePath { get; set; }
-
-        public Stream OpenReadStream(CancellationToken cancellationToken = default)
-            => Owner.OpenReadStream(this, cancellationToken);
-
-        public Task<IBrowserFile> ToImageFileAsync(string format, int maxWidth, int maxHeight)
-            => Owner.ConvertToImageFileAsync(this, format, maxWidth, maxHeight);
-    }
-}

+ 0 - 54
src/Components/Web.Extensions/src/InputFile/IBrowserFile.cs

@@ -1,54 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.IO;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace Microsoft.AspNetCore.Components.Web.Extensions
-{
-    /// <summary>
-    /// Represents the data of a file selected from an <see cref="InputFile"/> component.
-    /// </summary>
-    public interface IBrowserFile
-    {
-        /// <summary>
-        /// Gets the name of the file.
-        /// </summary>
-        string Name { get; }
-
-        /// <summary>
-        /// Gets the last modified date.
-        /// </summary>
-        DateTime LastModified { get; }
-
-        /// <summary>
-        /// Gets the size of the file in bytes.
-        /// </summary>
-        long Size { get; }
-
-        /// <summary>
-        /// Gets the MIME type of the file.
-        /// </summary>
-        string Type { get; }
-
-        /// <summary>
-        /// Opens the stream for reading the uploaded file.
-        /// </summary>
-        /// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
-        Stream OpenReadStream(CancellationToken cancellationToken = default);
-
-        /// <summary>
-        /// Converts the current image file to a new one of the specified file type and maximum file dimensions.
-        /// </summary>
-        /// <remarks>
-        /// The image will be scaled to fit the specified dimensions while preserving the original aspect ratio.
-        /// </remarks>
-        /// <param name="format">The new image format.</param>
-        /// <param name="maxWith">The maximum image width.</param>
-        /// <param name="maxHeight">The maximum image height</param>
-        /// <returns>A <see cref="Task"/> representing the completion of the operation.</returns>
-        Task<IBrowserFile> ToImageFileAsync(string format, int maxWith, int maxHeight);
-    }
-}

+ 0 - 28
src/Components/Web.Extensions/src/InputFile/InputFileChangeEventArgs.cs

@@ -1,28 +0,0 @@
-// Copyright (c) .NET Foundation. All rights reserved.
-// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-
-using System;
-using System.Collections.Generic;
-
-namespace Microsoft.AspNetCore.Components.Web.Extensions
-{
-    /// <summary>
-    /// Supplies information about an <see cref="InputFile.OnChange"/> event being raised.
-    /// </summary>
-    public class InputFileChangeEventArgs : EventArgs
-    {
-        /// <summary>
-        /// The updated file entries list.
-        /// </summary>
-        public IReadOnlyList<IBrowserFile> Files { get; }
-
-        /// <summary>
-        /// Constructs a new <see cref="InputFileChangeEventArgs"/> instance.
-        /// </summary>
-        /// <param name="files">The updated file entries list.</param>
-        public InputFileChangeEventArgs(IReadOnlyList<IBrowserFile> files)
-        {
-            Files = files;
-        }
-    }
-}

+ 1 - 1
src/Components/Web.Extensions/src/Microsoft.AspNetCore.Components.Web.Extensions.csproj

@@ -6,11 +6,11 @@
     <GenerateDocumentationFile>true</GenerateDocumentationFile>
     <RootNamespace>Microsoft.AspNetCore.Components</RootNamespace>
     <Nullable>enable</Nullable>
+    <IsShipping>false</IsShipping>
   </PropertyGroup>
 
   <ItemGroup>
     <Reference Include="Microsoft.AspNetCore.Components" />
-    <Reference Include="Microsoft.AspNetCore.DataProtection" />
     <Reference Include="Microsoft.JSInterop" />
   </ItemGroup>
 

+ 0 - 142
src/Components/Web.Extensions/src/wwwroot/inputFile.js

@@ -1,142 +0,0 @@
-(function () {
-
-    // Exported functions
-
-    function init(callbackWrapper, elem) {
-        elem._blazorInputFileNextFileId = 0;
-
-        elem.addEventListener('click', function () {
-            // Permits replacing an existing file with a new one of the same file name.
-            elem.value = '';
-        });
-
-        elem.addEventListener('change', function () {
-            // Reduce to purely serializable data, plus an index by ID.
-            elem._blazorFilesById = {};
-
-            const fileList = Array.prototype.map.call(elem.files, function (file) {
-                const result = {
-                    id: ++elem._blazorInputFileNextFileId,
-                    lastModified: new Date(file.lastModified).toISOString(),
-                    name: file.name,
-                    size: file.size,
-                    type: file.type,
-                };
-
-                elem._blazorFilesById[result.id] = result;
-
-                // Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
-                Object.defineProperty(result, 'blob', { value: file });
-
-                return result;
-            });
-
-            callbackWrapper.invokeMethodAsync('NotifyChange', fileList);
-        });
-    }
-
-    function toImageFile(elem, fileId, format, maxWidth, maxHeight) {
-        var originalFile = getFileById(elem, fileId);
-
-        return new Promise(function (resolve) {
-            var originalFileImage = new Image();
-            originalFileImage.onload = function () { resolve(originalFileImage); };
-            originalFileImage.src = URL.createObjectURL(originalFile.blob);
-        }).then(function (loadedImage) {
-            return new Promise(function (resolve) {
-                var desiredWidthRatio = Math.min(1, maxWidth / loadedImage.width);
-                var desiredHeightRatio = Math.min(1, maxHeight / loadedImage.height);
-                var chosenSizeRatio = Math.min(desiredWidthRatio, desiredHeightRatio);
-
-                var canvas = document.createElement('canvas');
-                canvas.width = Math.round(loadedImage.width * chosenSizeRatio);
-                canvas.height = Math.round(loadedImage.height * chosenSizeRatio);
-                canvas.getContext('2d').drawImage(loadedImage, 0, 0, canvas.width, canvas.height);
-                canvas.toBlob(resolve, format);
-            });
-        }).then(function (resizedImageBlob) {
-            var result = {
-                id: ++elem._blazorInputFileNextFileId,
-                lastModified: originalFile.lastModified,
-                name: originalFile.name, // Note: we're not changing the file extension.
-                size: resizedImageBlob.size,
-                type: format,
-                relativePath: originalFile.relativePath
-            };
-
-            elem._blazorFilesById[result.id] = result;
-
-            // Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
-            Object.defineProperty(result, 'blob', { value: resizedImageBlob });
-
-            return result;
-        });
-    }
-
-    function ensureArrayBufferReadyForSharedMemoryInterop(elem, fileId) {
-        return getArrayBufferFromFileAsync(elem, fileId).then(function (arrayBuffer) {
-            getFileById(elem, fileId).arrayBuffer = arrayBuffer;
-        });
-    }
-
-    function readFileData(elem, fileId, startOffset, count) {
-        return getArrayBufferFromFileAsync(elem, fileId).then(function (arrayBuffer) {
-            return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer, startOffset, count)));
-        });
-    }
-
-    function readFileDataSharedMemory(readRequest) {
-        const inputFileElementReferenceId = Blazor.platform.readStringField(readRequest, 0);
-        const inputFileElement = document.querySelector(`[_bl_${inputFileElementReferenceId}]`);
-        const fileId = Blazor.platform.readInt32Field(readRequest, 4);
-        const sourceOffset = Blazor.platform.readUint64Field(readRequest, 8);
-        const destination = Blazor.platform.readInt32Field(readRequest, 16);
-        const destinationOffset = Blazor.platform.readInt32Field(readRequest, 20);
-        const maxBytes = Blazor.platform.readInt32Field(readRequest, 24);
-
-        const sourceArrayBuffer = getFileById(inputFileElement, fileId).arrayBuffer;
-        const bytesToRead = Math.min(maxBytes, sourceArrayBuffer.byteLength - sourceOffset);
-        const sourceUint8Array = new Uint8Array(sourceArrayBuffer, sourceOffset, bytesToRead);
-
-        const destinationUint8Array = Blazor.platform.toUint8Array(destination);
-        destinationUint8Array.set(sourceUint8Array, destinationOffset);
-
-        return bytesToRead;
-    }
-
-    // Local helpers
-
-    function getFileById(elem, fileId) {
-        const file = elem._blazorFilesById[fileId];
-
-        if (!file) {
-            throw new Error(`There is no file with ID ${fileId}. The file list may have changed.`);
-        }
-
-        return file;
-    }
-
-    function getArrayBufferFromFileAsync(elem, fileId) {
-        const file = getFileById(elem, fileId);
-
-        // On the first read, convert the FileReader into a Promise<ArrayBuffer>.
-        if (!file.readPromise) {
-            file.readPromise = new Promise(function (resolve, reject) {
-                const reader = new FileReader();
-                reader.onload = function () { resolve(reader.result); };
-                reader.onerror = function (err) { reject(err); };
-                reader.readAsArrayBuffer(file.blob);
-            });
-        }
-
-        return file.readPromise;
-    }
-
-    window._blazorInputFile = {
-        init,
-        toImageFile,
-        ensureArrayBufferReadyForSharedMemoryInterop,
-        readFileData,
-        readFileDataSharedMemory,
-    };
-})();

File diff suppressed because it is too large
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.server.js


File diff suppressed because it is too large
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.webassembly.js


+ 2 - 0
src/Components/Web.JS/src/GlobalExports.ts

@@ -2,6 +2,7 @@ import { navigateTo, internalFunctions as navigationManagerInternalFunctions } f
 import { attachRootComponentToElement } from './Rendering/Renderer';
 import { domFunctions } from './DomWrapper';
 import { Virtualize } from './Virtualize';
+import { InputFile } from './InputFile';
 
 // Make the following APIs available in global scope for invocation from JS
 window['Blazor'] = {
@@ -11,5 +12,6 @@ window['Blazor'] = {
     navigationManager: navigationManagerInternalFunctions,
     domWrapper: domFunctions,
     Virtualize,
+    InputFile,
   },
 };

+ 159 - 0
src/Components/Web.JS/src/InputFile.ts

@@ -0,0 +1,159 @@
+import { monoPlatform } from './Platform/Mono/MonoPlatform';
+import { System_Array } from './Platform/Platform';
+
+export const InputFile = {
+  init,
+  toImageFile,
+  ensureArrayBufferReadyForSharedMemoryInterop,
+  readFileData,
+  readFileDataSharedMemory,
+};
+
+interface BrowserFile {
+  id: number;
+  lastModified: string;
+  name: string;
+  size: number;
+  type: string;
+  readPromise: Promise<ArrayBuffer> | undefined;
+  arrayBuffer: ArrayBuffer | undefined;
+}
+
+interface InputElement extends HTMLInputElement {
+  _blazorInputFileNextFileId: number;
+  _blazorFilesById: { [id: number]: BrowserFile };
+}
+
+function init(callbackWrapper: any, elem: InputElement): void {
+  elem._blazorInputFileNextFileId = 0;
+
+  elem.addEventListener('click', function(): void {
+    // Permits replacing an existing file with a new one of the same file name.
+    elem.value = '';
+  });
+
+  elem.addEventListener('change', function(): void {
+    // Reduce to purely serializable data, plus an index by ID.
+    elem._blazorFilesById = {};
+
+    const fileList = Array.prototype.map.call(elem.files, function(file): BrowserFile {
+      const result = {
+        id: ++elem._blazorInputFileNextFileId,
+        lastModified: new Date(file.lastModified).toISOString(),
+        name: file.name,
+        size: file.size,
+        type: file.type,
+        readPromise: undefined,
+        arrayBuffer: undefined,
+      };
+
+      elem._blazorFilesById[result.id] = result;
+
+      // Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
+      Object.defineProperty(result, 'blob', { value: file });
+
+      return result;
+    });
+
+    callbackWrapper.invokeMethodAsync('NotifyChange', fileList);
+  });
+}
+
+async function toImageFile(elem: InputElement, fileId: number, format: string, maxWidth: number, maxHeight: number): Promise<BrowserFile> {
+  const originalFile = getFileById(elem, fileId);
+
+  const loadedImage = await new Promise(function(resolve: (loadedImage: HTMLImageElement) => void): void {
+    const originalFileImage = new Image();
+    originalFileImage.onload = function(): void {
+      resolve(originalFileImage);
+    };
+    originalFileImage.src = URL.createObjectURL(originalFile['blob']);
+  });
+
+  const resizedImageBlob = await new Promise(function(resolve: BlobCallback): void {
+    const desiredWidthRatio = Math.min(1, maxWidth / loadedImage.width);
+    const desiredHeightRatio = Math.min(1, maxHeight / loadedImage.height);
+    const chosenSizeRatio = Math.min(desiredWidthRatio, desiredHeightRatio);
+
+    const canvas = document.createElement('canvas');
+    canvas.width = Math.round(loadedImage.width * chosenSizeRatio);
+    canvas.height = Math.round(loadedImage.height * chosenSizeRatio);
+    canvas.getContext('2d')?.drawImage(loadedImage, 0, 0, canvas.width, canvas.height);
+    canvas.toBlob(resolve, format);
+  });
+  const result: BrowserFile = {
+    id: ++elem._blazorInputFileNextFileId,
+    lastModified: originalFile.lastModified,
+    name: originalFile.name,
+    size: resizedImageBlob?.size || 0,
+    type: format,
+    readPromise: undefined,
+    arrayBuffer: undefined,
+  };
+
+  elem._blazorFilesById[result.id] = result;
+
+  // Attach the blob data itself as a non-enumerable property so it doesn't appear in the JSON.
+  Object.defineProperty(result, 'blob', { value: resizedImageBlob });
+
+  return result;
+}
+
+async function ensureArrayBufferReadyForSharedMemoryInterop(elem: InputElement, fileId: number): Promise<void> {
+  const arrayBuffer = await getArrayBufferFromFileAsync(elem, fileId);
+  getFileById(elem, fileId).arrayBuffer = arrayBuffer;
+}
+
+async function readFileData(elem: InputElement, fileId: number, startOffset: number, count: number): Promise<string> {
+  const arrayBuffer = await getArrayBufferFromFileAsync(elem, fileId);
+  return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer, startOffset, count) as unknown as number[]));
+}
+
+function readFileDataSharedMemory(readRequest: any): number {
+  const inputFileElementReferenceId = monoPlatform.readStringField(readRequest, 0);
+  const inputFileElement = document.querySelector(`[_bl_${inputFileElementReferenceId}]`);
+  const fileId = monoPlatform.readInt32Field(readRequest, 4);
+  const sourceOffset = monoPlatform.readUint64Field(readRequest, 8);
+  const destination = monoPlatform.readInt32Field(readRequest, 16) as unknown as System_Array<number>;
+  const destinationOffset = monoPlatform.readInt32Field(readRequest, 20);
+  const maxBytes = monoPlatform.readInt32Field(readRequest, 24);
+
+  const sourceArrayBuffer = getFileById(inputFileElement as InputElement, fileId).arrayBuffer as ArrayBuffer;
+  const bytesToRead = Math.min(maxBytes, sourceArrayBuffer.byteLength - sourceOffset);
+  const sourceUint8Array = new Uint8Array(sourceArrayBuffer, sourceOffset, bytesToRead);
+
+  const destinationUint8Array = monoPlatform.toUint8Array(destination);
+  destinationUint8Array.set(sourceUint8Array, destinationOffset);
+
+  return bytesToRead;
+}
+
+function getFileById(elem: InputElement, fileId: number): BrowserFile {
+  const file = elem._blazorFilesById[fileId];
+
+  if (!file) {
+    throw new Error(`There is no file with ID ${fileId}. The file list may have changed.`);
+  }
+
+  return file;
+}
+
+function getArrayBufferFromFileAsync(elem: InputElement, fileId: number): Promise<ArrayBuffer> {
+  const file = getFileById(elem, fileId);
+
+  // On the first read, convert the FileReader into a Promise<ArrayBuffer>.
+  if (!file.readPromise) {
+    file.readPromise = new Promise(function(resolve: (buffer: ArrayBuffer) => void, reject): void {
+      const reader = new FileReader();
+      reader.onload = function(): void {
+        resolve(reader.result as ArrayBuffer);
+      };
+      reader.onerror = function(err): void {
+        reject(err);
+      };
+      reader.readAsArrayBuffer(file['blob']);
+    });
+  }
+
+  return file.readPromise;
+}

+ 3 - 3
src/Components/Web.Extensions/src/InputFile/InputFile.cs → src/Components/Web/src/Forms/InputFile.cs

@@ -10,10 +10,10 @@ using Microsoft.AspNetCore.Components.Rendering;
 using Microsoft.Extensions.Options;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     /// <summary>
-    /// A component that wraps the HTML file input element and exposes a <see cref="Stream"/> for each file's contents.
+    /// A component that wraps the HTML file input element and supplies a <see cref="Stream"/> for each file's contents.
     /// </summary>
     public class InputFile : ComponentBase, IInputFileJsCallbacks, IDisposable
     {
@@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
                 (Stream)new SharedBrowserFileStream(JSRuntime, _jsUnmarshalledRuntime, _inputFileElement, file) :
                 new RemoteBrowserFileStream(JSRuntime, _inputFileElement, file, Options.Value, cancellationToken);
 
-        internal async Task<IBrowserFile> ConvertToImageFileAsync(BrowserFile file, string format, int maxWidth, int maxHeight)
+        internal async ValueTask<IBrowserFile> ConvertToImageFileAsync(BrowserFile file, string format, int maxWidth, int maxHeight)
         {
             var imageFile = await JSRuntime.InvokeAsync<BrowserFile>(InputFileInterop.ToImageFile, _inputFileElement, file.Id, format, maxWidth, maxHeight);
 

+ 50 - 0
src/Components/Web/src/Forms/InputFile/BrowserFile.cs

@@ -0,0 +1,50 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    internal sealed class BrowserFile : IBrowserFile
+    {
+        private long _size;
+
+        internal InputFile Owner { get; set; } = default!;
+
+        public int Id { get; set; }
+
+        public string Name { get; set; } = string.Empty;
+
+        public DateTimeOffset LastModified { get; set; }
+
+        public long Size
+        {
+            get => _size;
+            set
+            {
+                if (value < 0)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(Size), $"Size must be a non-negative value. Value provided: {value}.");
+                }
+
+                _size = value;
+            }
+        }
+
+        public string ContentType { get; set; } = string.Empty;
+
+        public string? RelativePath { get; set; }
+
+        public Stream OpenReadStream(long maxAllowedSize = 512000, CancellationToken cancellationToken = default)
+        {
+            if (Size > maxAllowedSize)
+            {
+                throw new IOException($"Supplied file with size {Size} bytes exceeds the maximum of {maxAllowedSize} bytes.");
+            }
+
+            return Owner.OpenReadStream(this, cancellationToken);
+        }
+    }
+}

+ 40 - 0
src/Components/Web/src/Forms/InputFile/BrowserFileExtensions.cs

@@ -0,0 +1,40 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    /// <summary>
+    /// Contains helper methods for <see cref="IBrowserFile"/>.
+    /// </summary>
+    public static class BrowserFileExtensions
+    {
+        /// <summary>
+        /// Attempts to convert the current image file to a new one of the specified file type and maximum file dimensions.
+        /// <para>
+        /// Caution: there is no guarantee that the file will be converted, or will even be a valid image file at all, either
+        /// before or after conversion. The conversion is requested within the browser before it is transferred to .NET
+        /// code, so the resulting data should be treated as untrusted.
+        /// </para>
+        /// </summary>
+        /// <remarks>
+        /// The image will be scaled to fit the specified dimensions while preserving the original aspect ratio.
+        /// </remarks>
+        /// <param name="browserFile">The <see cref="IBrowserFile"/> to convert to a new image file.</param>
+        /// <param name="format">The new image format.</param>
+        /// <param name="maxWith">The maximum image width.</param>
+        /// <param name="maxHeight">The maximum image height</param>
+        /// <returns>A <see cref="ValueTask"/> representing the completion of the operation.</returns>
+        public static ValueTask<IBrowserFile> RequestImageFileAsync(this IBrowserFile browserFile, string format, int maxWith, int maxHeight)
+        {
+            if (browserFile is BrowserFile browserFileInternal)
+            {
+                return browserFileInternal.Owner.ConvertToImageFileAsync(browserFileInternal, format, maxWith, maxHeight);
+            }
+
+            throw new InvalidOperationException($"Cannot perform this operation on custom {typeof(IBrowserFile)} implementations.");
+        }
+    }
+}

+ 1 - 1
src/Components/Web.Extensions/src/InputFile/BrowserFileStream.cs → src/Components/Web/src/Forms/InputFile/BrowserFileStream.cs

@@ -6,7 +6,7 @@ using System.IO;
 using System.Threading;
 using System.Threading.Tasks;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     internal abstract class BrowserFileStream : Stream
     {

+ 62 - 0
src/Components/Web/src/Forms/InputFile/IBrowserFile.cs

@@ -0,0 +1,62 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Threading;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    /// <summary>
+    /// Represents the data of a file selected from an <see cref="InputFile"/> component.
+    /// <para>
+    /// Note: Metadata is provided by the client and is untrusted.
+    /// </para>
+    /// </summary>
+    public interface IBrowserFile
+    {
+        /// <summary>
+        /// Gets the name of the file as specified by the browser.
+        /// </summary>
+        string Name { get; }
+
+        /// <summary>
+        /// Gets the last modified date as specified by the browser.
+        /// </summary>
+        DateTimeOffset LastModified { get; }
+
+        /// <summary>
+        /// Gets the size of the file in bytes as specified by the browser.
+        /// </summary>
+        long Size { get; }
+
+        /// <summary>
+        /// Gets the MIME type of the file as specified by the browser.
+        /// </summary>
+        string ContentType { get; }
+
+        /// <summary>
+        /// Opens the stream for reading the uploaded file.
+        /// </summary>
+        /// <param name="maxAllowedSize">
+        /// The maximum number of bytes that can be supplied by the Stream. Defaults to 500 KB.
+        /// <para>
+        /// Calling <see cref="OpenReadStream(long, CancellationToken)"/>
+        /// will throw if the file's size, as specified by <see cref="Size"/> is larger than
+        /// <paramref name="maxAllowedSize"/>. By default, if the user supplies a file larger than 500 KB, this method will throw an exception.
+        /// </para>
+        /// <para>
+        /// It is valuable to choose a size limit that corresponds to your use case. If you allow excessively large files, this
+        /// may result in excessive consumption of memory or disk/database space, depending on what your code does
+        /// with the supplied <see cref="Stream"/>.
+        /// </para>
+        /// <para>
+        /// For Blazor Server in particular, beware of reading the entire stream into a memory buffer unless you have
+        /// passed a suitably low size limit, since you will be consuming that memory on the server.
+        /// </para>
+        /// </param>
+        /// <param name="cancellationToken">A cancellation token to signal the cancellation of streaming file data.</param>
+        /// <exception cref="IOException">Thrown if the file's length exceeds the <paramref name="maxAllowedSize"/> value.</exception>
+        Stream OpenReadStream(long maxAllowedSize = 500 * 1024, CancellationToken cancellationToken = default);
+    }
+}

+ 1 - 1
src/Components/Web.Extensions/src/InputFile/IInputFileJsCallbacks.cs → src/Components/Web/src/Forms/InputFile/IInputFileJsCallbacks.cs

@@ -3,7 +3,7 @@
 
 using System.Threading.Tasks;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     internal interface IInputFileJsCallbacks
     {

+ 57 - 0
src/Components/Web/src/Forms/InputFile/InputFileChangeEventArgs.cs

@@ -0,0 +1,57 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    /// <summary>
+    /// Supplies information about an <see cref="InputFile.OnChange"/> event being raised.
+    /// </summary>
+    public sealed class InputFileChangeEventArgs : EventArgs
+    {
+        private readonly IReadOnlyList<IBrowserFile> _files;
+
+        /// <summary>
+        /// Constructs a new <see cref="InputFileChangeEventArgs"/> instance.
+        /// </summary>
+        /// <param name="files">The list of <see cref="IBrowserFile"/>.</param>
+        public InputFileChangeEventArgs(IReadOnlyList<IBrowserFile> files)
+        {
+            _files = files ?? throw new ArgumentNullException(nameof(files));
+        }
+
+        /// <summary>
+        /// Gets the number of supplied files.
+        /// </summary>
+        public int FileCount => _files.Count;
+
+        /// <summary>
+        /// Gets the supplied file. Note that if the input accepts multiple files, then instead of
+        /// reading this property, you should call <see cref="GetMultipleFiles(int)"/>.
+        /// </summary>
+        public IBrowserFile File => _files.Count switch
+        {
+            0 => throw new InvalidOperationException("No file was supplied."),
+            1 => _files[0],
+            _ => throw new InvalidOperationException($"More than one file was supplied. Call {nameof(GetMultipleFiles)} to receive multiple files."),
+        };
+
+        /// <summary>
+        /// Gets the file entries list. This method should be used for inputs that accept multiple
+        /// files. If the input accepts only a single file, then use the <see cref="File"/> property
+        /// instead.
+        /// </summary>
+        /// <param name="maximumFileCount">The maximum number of files to accept. If the number of files exceeds this value, this method will throw an exception.</param>
+        public IReadOnlyList<IBrowserFile> GetMultipleFiles(int maximumFileCount = 10)
+        {
+            if (_files.Count > maximumFileCount)
+            {
+                throw new InvalidOperationException($"The maximum number of files accepted is {maximumFileCount}, but {_files.Count} were supplied.");
+            }
+
+            return _files;
+        }
+    }
+}

+ 2 - 2
src/Components/Web.Extensions/src/InputFile/InputFileInterop.cs → src/Components/Web/src/Forms/InputFile/InputFileInterop.cs

@@ -1,11 +1,11 @@
 // Copyright (c) .NET Foundation. All rights reserved.
 // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     internal static class InputFileInterop
     {
-        private const string JsFunctionsPrefix = "_blazorInputFile.";
+        private const string JsFunctionsPrefix = "Blazor._internal.InputFile.";
 
         public const string Init = JsFunctionsPrefix + "init";
 

+ 1 - 1
src/Components/Web.Extensions/src/InputFile/InputFileJsCallbacksRelay.cs → src/Components/Web/src/Forms/InputFile/InputFileJsCallbacksRelay.cs

@@ -5,7 +5,7 @@ using System;
 using System.Threading.Tasks;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     internal class InputFileJsCallbacksRelay : IDisposable
     {

+ 1 - 1
src/Components/Web.Extensions/src/InputFile/ReadRequest.cs → src/Components/Web/src/Forms/InputFile/ReadRequest.cs

@@ -3,7 +3,7 @@
 
 using System.Runtime.InteropServices;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     [StructLayout(LayoutKind.Explicit)]
     internal struct ReadRequest

+ 2 - 2
src/Components/Web.Extensions/src/InputFile/RemoteBrowserFileStream.cs → src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStream.cs

@@ -8,7 +8,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     internal class RemoteBrowserFileStream : BrowserFileStream
     {
@@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
         {
             _jsRuntime = jsRuntime;
             _inputFileElement = inputFileElement;
-            _maxSegmentSize = options.SegmentSize;
+            _maxSegmentSize = options.MaxSegmentSize;
             _segmentFetchTimeout = options.SegmentFetchTimeout;
 
             var pipe = new Pipe(new PipeOptions(pauseWriterThreshold: options.MaxBufferSize, resumeWriterThreshold: options.MaxBufferSize));

+ 3 - 3
src/Components/Web.Extensions/src/InputFile/RemoteBrowserFileStreamOptions.cs → src/Components/Web/src/Forms/InputFile/RemoteBrowserFileStreamOptions.cs

@@ -4,7 +4,7 @@
 using System;
 using System.Runtime.Versioning;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     /// <summary>
     /// Repesents configurable options for <see cref="RemoteBrowserFileStream"/>.
@@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
         /// This only has an effect when using Blazor Server.
         /// </para>
         /// </summary>
-        public int SegmentSize { get; set; } = 20 * 1024; // SignalR limit is 32K.
+        public int MaxSegmentSize { get; set; } = 20 * 1024; // SignalR limit is 32K.
 
         /// <summary>
         /// Gets or sets the maximum internal buffer size for unread data sent over a SignalR circuit.
@@ -35,6 +35,6 @@ namespace Microsoft.AspNetCore.Components.Web.Extensions
         /// This only has an effect when using Blazor Server.
         /// </para>
         /// </summary>
-        public TimeSpan SegmentFetchTimeout { get; set; } = TimeSpan.FromSeconds(3);
+        public TimeSpan SegmentFetchTimeout { get; set; } = TimeSpan.FromMinutes(1);
     }
 }

+ 1 - 1
src/Components/Web.Extensions/src/InputFile/SharedBrowserFileStream.cs → src/Components/Web/src/Forms/InputFile/SharedBrowserFileStream.cs

@@ -7,7 +7,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using Microsoft.JSInterop;
 
-namespace Microsoft.AspNetCore.Components.Web.Extensions
+namespace Microsoft.AspNetCore.Components.Forms
 {
     internal class SharedBrowserFileStream : BrowserFileStream
     {

+ 33 - 0
src/Components/Web/test/Forms/BrowserFileTest.cs

@@ -0,0 +1,33 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    public class BrowserFileTest
+    {
+        [Fact]
+        public void SetSize_ThrowsIfSizeIsNegative()
+        {
+            // Arrange
+            var file = new BrowserFile();
+
+            // Act & Assert
+            var ex = Assert.Throws<ArgumentOutOfRangeException>(() => file.Size = -7);
+        }
+
+        [Fact]
+        public void OpenReadStream_ThrowsIfFileSizeIsLargerThanAllowedSize()
+        {
+            // Arrange
+            var file = new BrowserFile { Size = 100 };
+
+            // Act & Assert
+            var ex = Assert.Throws<IOException>(() => file.OpenReadStream(80));
+            Assert.Equal("Supplied file with size 100 bytes exceeds the maximum of 80 bytes.", ex.Message);
+        }
+    }
+}

+ 69 - 0
src/Components/Web/test/Forms/InputFileChangeEventArgsTest.cs

@@ -0,0 +1,69 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+
+namespace Microsoft.AspNetCore.Components.Forms
+{
+    public class InputFileChangeEventArgsTest
+    {
+        [Fact]
+        public void SuppliesNumberOfFiles()
+        {
+            var emptySet = new InputFileChangeEventArgs(Array.Empty<IBrowserFile>());
+            Assert.Equal(0, emptySet.FileCount);
+
+            var twoItemSet = new InputFileChangeEventArgs(new[] { new BrowserFile(), new BrowserFile() });
+            Assert.Equal(2, twoItemSet.FileCount);
+        }
+
+        [Fact]
+        public void File_CanSupplySingle()
+        {
+            var file = new BrowserFile();
+            var instance = new InputFileChangeEventArgs(new[] { file });
+            Assert.Same(file, instance.File);
+        }
+
+        [Fact]
+        public void File_ThrowsIfZeroFiles()
+        {
+            var instance = new InputFileChangeEventArgs(Array.Empty<IBrowserFile>());
+            var ex = Assert.Throws<InvalidOperationException>(() => instance.File);
+            Assert.StartsWith("No file was supplied", ex.Message);
+        }
+
+        [Fact]
+        public void File_ThrowsIfMultipleFiles()
+        {
+            var instance = new InputFileChangeEventArgs(new[] { new BrowserFile(), new BrowserFile() });
+            var ex = Assert.Throws<InvalidOperationException>(() => instance.File);
+            Assert.StartsWith("More than one file was supplied", ex.Message);
+        }
+
+        [Fact]
+        public void GetMultipleFiles_CanSupplyEmpty()
+        {
+            var instance = new InputFileChangeEventArgs(Array.Empty<IBrowserFile>());
+            Assert.Empty(instance.GetMultipleFiles());
+        }
+
+        [Fact]
+        public void GetMultipleFiles_CanSupplyFiles()
+        {
+            var files = new[] { new BrowserFile(), new BrowserFile() };
+            var instance = new InputFileChangeEventArgs(files);
+            Assert.Same(files, instance.GetMultipleFiles());
+        }
+
+        [Fact]
+        public void GetMultipleFiles_ThrowsIfTooManyFiles()
+        {
+            var files = new[] { new BrowserFile(), new BrowserFile() };
+            var instance = new InputFileChangeEventArgs(files);
+            var ex = Assert.Throws<InvalidOperationException>(() => instance.GetMultipleFiles(1));
+            Assert.Equal($"The maximum number of files accepted is 1, but 2 were supplied.", ex.Message);
+        }
+    }
+}

+ 8 - 0
src/Components/test/E2ETest/ServerExecutionTests/TestSubclasses.cs

@@ -75,4 +75,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
         {
         }
     }
+
+    public class ServerInputFileTest : InputFileTest
+    {
+        public ServerInputFileTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
+            : base(browserFixture, serverFixture.WithServerExecution(), output)
+        {
+        }
+    }
 }

+ 41 - 1
src/Components/test/E2ETest/Tests/InputFileTest.cs

@@ -6,6 +6,7 @@ using System.IO;
 using System.Linq;
 using System.Text;
 using BasicTestApp;
+using BasicTestApp.FormsTest;
 using Microsoft.AspNetCore.Components.E2ETest;
 using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
 using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
@@ -15,7 +16,7 @@ using OpenQA.Selenium.Support.Extensions;
 using Xunit;
 using Xunit.Abstractions;
 
-namespace Microsoft.AspNetCore.Components.E2ETests.Tests
+namespace Microsoft.AspNetCore.Components.E2ETest.Tests
 {
     public class InputFileTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>, IDisposable
     {
@@ -139,6 +140,45 @@ namespace Microsoft.AspNetCore.Components.E2ETests.Tests
             Browser.Equal(480, () => uploadedImage.Size.Height);
         }
 
+        [Fact]
+        public void ThrowsWhenTooManyFilesAreSelected()
+        {
+            var maxAllowedFilesElement = Browser.FindElement(By.Id("max-allowed-files"));
+            maxAllowedFilesElement.Clear();
+            maxAllowedFilesElement.SendKeys("1\n");
+
+            // Save two files locally
+            var file1 = TempFile.Create(_tempDirectory, "txt", "This is file 1.");
+            var file2 = TempFile.Create(_tempDirectory, "txt", "This is file 2.");
+
+            // Select both files
+            var inputFile = Browser.FindElement(By.Id("input-file"));
+            inputFile.SendKeys($"{file1.Path}\n{file2.Path}");
+
+            // Validate that the proper exception is thrown
+            var exceptionMessage = Browser.FindElement(By.Id("exception-message"));
+            Browser.Equal("The maximum number of files accepted is 1, but 2 were supplied.", () => exceptionMessage.Text);
+        }
+
+        [Fact]
+        public void ThrowsWhenOversizedFileIsSelected()
+        {
+            var maxFileSizeElement = Browser.FindElement(By.Id("max-file-size"));
+            maxFileSizeElement.Clear();
+            maxFileSizeElement.SendKeys("10\n");
+
+            // Save a file that exceeds the specified file size limit
+            var file = TempFile.Create(_tempDirectory, "txt", "This file is over 10 bytes long.");
+
+            // Select the file
+            var inputFile = Browser.FindElement(By.Id("input-file"));
+            inputFile.SendKeys(file.Path);
+
+            // Validate that the proper exception is thrown
+            var exceptionMessage = Browser.FindElement(By.Id("exception-message"));
+            Browser.Equal("Supplied file with size 32 bytes exceeds the maximum of 10 bytes.", () => exceptionMessage.Text);
+        }
+
         public void Dispose()
         {
             Directory.Delete(_tempDirectory, recursive: true);

+ 1 - 0
src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj

@@ -15,6 +15,7 @@
     <Reference Include="System.Net.Http.Json" />
     <Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
     <Reference Include="Microsoft.AspNetCore.Components.Authorization" />
+    <Reference Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage" />
     <Reference Include="Microsoft.AspNetCore.Components.Web.Extensions" />
     <Reference Include="Microsoft.Extensions.Logging.Configuration" />
     <Reference Include="Newtonsoft.Json" />

+ 102 - 0
src/Components/test/testassets/BasicTestApp/FormsTest/InputFileComponent.razor

@@ -0,0 +1,102 @@
+@using System.IO;
+@using Microsoft.AspNetCore.Components.Forms
+
+<h1>File preview</h1>
+
+Max file size:
+<br />
+<input type="number" id="max-file-size" @bind-value="@maxFileSize" />
+<br />
+
+Max allowed files:
+<br />
+<input type="number" id="max-allowed-files" @bind-value="@maxAllowedFiles" />
+<br />
+
+<InputFile OnChange="LoadFiles" id="input-file" multiple />
+<br />
+
+<span id="exception-message">@exceptionMessage</span>
+
+@if (isLoading)
+{
+    <p>Loading...</p>
+    <br />
+}
+
+@foreach (var (file, content) in loadedFiles)
+{
+    <p id="file-@(file.Name)">
+        <strong>File name:</strong> @(file.Name)<br />
+        <strong>File size (bytes):</strong> <span id="file-size">@(file.Size)</span><br />
+        <strong>File content:</strong> <span id="file-content">@content</span><br />
+    </p>
+}
+
+<h1>Image upload</h1>
+
+<InputFile OnChange="LoadImage" id="input-image" />
+<br />
+
+@if (imageDataUri != null)
+{
+    <p>
+        Uploaded image:<br />
+        <img id="image-uploaded" src="@imageDataUri" />
+    </p>
+}
+
+<p>
+    Source image:<br />
+    <img id="image-source" src="images/blazor_logo_1000x.png" />
+</p>
+
+@code {
+    Dictionary<IBrowserFile, string> loadedFiles = new Dictionary<IBrowserFile, string>();
+
+    long maxFileSize = 1024 * 1024 * 15;
+    int maxAllowedFiles = 3;
+
+    bool isLoading;
+
+    string imageDataUri;
+
+    string exceptionMessage;
+
+    async Task LoadFiles(InputFileChangeEventArgs e)
+    {
+        isLoading = true;
+        loadedFiles.Clear();
+        exceptionMessage = string.Empty;
+
+        try
+        {
+            foreach (var file in e.GetMultipleFiles(maxAllowedFiles))
+            {
+                StateHasChanged();
+
+                using var reader = new StreamReader(file.OpenReadStream(maxFileSize));
+
+                loadedFiles.Add(file, await reader.ReadToEndAsync());
+            }
+        }
+        catch (Exception ex)
+        {
+            exceptionMessage = ex.Message;
+        }
+
+        isLoading = false;
+    }
+
+    async Task LoadImage(InputFileChangeEventArgs e)
+    {
+        var format = "image/jpeg";
+        var imageFile = await e.File.RequestImageFileAsync(format, 640, 480);
+
+        using var fileStream = imageFile.OpenReadStream(maxFileSize);
+        using var memoryStream = new MemoryStream();
+        await fileStream.CopyToAsync(memoryStream);
+
+        imageDataUri = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";
+    }
+}

+ 1 - 1
src/Components/test/testassets/BasicTestApp/Index.razor

@@ -36,6 +36,7 @@
         <option value="BasicTestApp.FormsTest.SimpleValidationComponentUsingExperimentalValidator">Simple validation using experimental validator</option>
         <option value="BasicTestApp.FormsTest.TypicalValidationComponent">Typical validation</option>
         <option value="BasicTestApp.FormsTest.TypicalValidationComponentUsingExperimentalValidator">Typical validation using experimental validator</option>
+        <option value="BasicTestApp.FormsTest.InputFileComponent">Input file</option>
         <option value="BasicTestApp.NavigateOnSubmit">Navigate to submit</option>
         <option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
         <option value="BasicTestApp.HierarchicalImportsTest.Subdir.ComponentUsingImports">Imports statement</option>
@@ -46,7 +47,6 @@
         <option value="BasicTestApp.HttpClientTest.CookieCounterComponent">HttpClient cookies</option>
         <option value="BasicTestApp.HttpClientTest.HttpRequestsComponent">HttpClient tester</option>
         <option value="BasicTestApp.InputEventComponent">Input events</option>
-        <option value="BasicTestApp.InputFileComponent">Input file</option>
         <option value="BasicTestApp.InteropComponent">Interop component</option>
         <option value="BasicTestApp.InteropOnInitializationComponent">Interop on initialization</option>
         <option value="BasicTestApp.JsonSerializationCases">JSON serialization</option>

+ 0 - 80
src/Components/test/testassets/BasicTestApp/InputFileComponent.razor

@@ -1,80 +0,0 @@
-@using System.IO;
-@using Microsoft.AspNetCore.Components.Web.Extensions
-
-<h1>File preview</h1>
-
-<InputFile OnChange="LoadFiles" id="input-file" multiple /><br />
-
-@if (isLoading)
-{
-    <p>Loading...</p><br />
-}
-
-@foreach (var (file, content) in loadedFiles)
-{
-    <p id="file-@(file.Name)">
-        <strong>File name:</strong> @(file.Name)<br />
-        <strong>File size (bytes):</strong> <span id="file-size">@(file.Size)</span><br />
-        <strong>File content:</strong> <span id="file-content">@content</span><br />
-    </p>
-}
-
-<h1>Image upload</h1>
-
-<InputFile OnChange="LoadImage" id="input-image" /><br />
-
-@if (imageDataUri != null)
-{
-    <p>
-        Uploaded image:<br />
-        <img id="image-uploaded" src="@imageDataUri" />
-    </p>
-}
-
-<p>
-    Source image:<br />
-    <img id="image-source" src="images/blazor_logo_1000x.png" />
-</p>
-
-@code {
-    Dictionary<IBrowserFile, string> loadedFiles = new Dictionary<IBrowserFile, string>();
-
-    bool isLoading;
-
-    string imageDataUri;
-
-    async Task LoadFiles(InputFileChangeEventArgs e)
-    {
-        isLoading = true;
-        loadedFiles.Clear();
-
-        foreach (var file in e.Files)
-        {
-            StateHasChanged();
-
-            using var reader = new StreamReader(file.OpenReadStream());
-
-            loadedFiles.Add(file, await reader.ReadToEndAsync());
-        }
-
-        isLoading = false;
-    }
-
-    async Task LoadImage(InputFileChangeEventArgs e)
-    {
-        var file = e.Files.SingleOrDefault();
-
-        if (file != null)
-        {
-            var format = "image/jpeg";
-            var imageFile = await file.ToImageFileAsync(format, 640, 480);
-
-            using var fileStream = imageFile.OpenReadStream();
-            using var memoryStream = new MemoryStream();
-            await fileStream.CopyToAsync(memoryStream);
-
-            imageDataUri = $"data:{format};base64,{Convert.ToBase64String(memoryStream.ToArray())}";
-            StateHasChanged();
-        }
-    }
-}

+ 1 - 0
src/Components/test/testassets/BasicTestApp/Program.cs

@@ -19,6 +19,7 @@ using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.Logging;
 using Microsoft.Extensions.Logging.Configuration;
 using Microsoft.JSInterop;
+using Microsoft.AspNetCore.Components.ProtectedBrowserStorage;
 
 namespace BasicTestApp
 {

+ 1 - 0
src/Components/test/testassets/BasicTestApp/ProtectedBrowserStorageInjectionComponent.razor

@@ -1,5 +1,6 @@
 @using Microsoft.Extensions.DependencyInjection 
 @using Microsoft.AspNetCore.Components.Web.Extensions
+@using Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 @inject IServiceProvider ServiceProvider
 
 <button id="inject-local" @onclick="Inject<ProtectedLocalStorage>">Inject @(nameof(ProtectedLocalStorage))</button>

+ 1 - 0
src/Components/test/testassets/BasicTestApp/ProtectedBrowserStorageUsageComponent.razor

@@ -1,4 +1,5 @@
 @using Microsoft.AspNetCore.Components.Web.Extensions
+@using Microsoft.AspNetCore.Components.ProtectedBrowserStorage
 @inject ProtectedLocalStorage ProtectedLocalStore
 @inject ProtectedSessionStorage ProtectedSessionStore
 

+ 0 - 2
src/Components/test/testassets/BasicTestApp/wwwroot/index.html

@@ -45,8 +45,6 @@
 
     <script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/headManager.js"></script>
 
-    <script src="_content/Microsoft.AspNetCore.Components.Web.Extensions/inputFile.js"></script>
-
     <!-- Used by ExternalContentPackage -->
     <script src="_content/TestContentPackage/prompt.js"></script>
 </body>

Some files were not shown because too many files changed in this diff