Explorar o código

asp.net core新增支持断点续传和支持多线程下载的ResumeFileResult

懒得勤快 %!s(int64=6) %!d(string=hai) anos
pai
achega
4b0a533ec7
Modificáronse 49 ficheiros con 2763 adicións e 166 borrados
  1. 139 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Controllers/TestController.cs
  2. 35 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj
  3. 19 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.xml
  4. 24 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Program.cs
  5. 28 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Properties/launchSettings.json
  6. 65 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Startup.cs
  7. 9 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/appsettings.Development.json
  8. 8 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/appsettings.json
  9. 1 0
      Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/wwwroot/TestFile.txt
  10. 244 0
      Masuit.Tools.Core.UnitTest/AspNetCore/FileContentsTests.cs
  11. 244 0
      Masuit.Tools.Core.UnitTest/AspNetCore/FileStreamTests.cs
  12. 244 0
      Masuit.Tools.Core.UnitTest/AspNetCore/PhysicalFileTests.cs
  13. 648 0
      Masuit.Tools.Core.UnitTest/AspNetCore/PreconditionTests.cs
  14. 32 0
      Masuit.Tools.Core.UnitTest/AspNetCore/TestBase.cs
  15. 246 0
      Masuit.Tools.Core.UnitTest/AspNetCore/VirtualFileTests.cs
  16. 2 0
      Masuit.Tools.Core.UnitTest/Masuit.Tools.Core.UnitTest.csproj
  17. 1 0
      Masuit.Tools.Core.UnitTest/wwwroot/TestFile.txt
  18. 46 0
      Masuit.Tools.Core/AspNetCore/Executor/ResumeFileContentResultExecutor.cs
  19. 47 0
      Masuit.Tools.Core/AspNetCore/Executor/ResumeFileStreamResultExecutor.cs
  20. 46 0
      Masuit.Tools.Core/AspNetCore/Executor/ResumePhysicalFileResultExecutor.cs
  21. 49 0
      Masuit.Tools.Core/AspNetCore/Executor/ResumeVirtualFileResultExecutor.cs
  22. 32 0
      Masuit.Tools.Core/AspNetCore/Extensions/ActionContextExtension.cs
  23. 180 0
      Masuit.Tools.Core/AspNetCore/Extensions/ControllerExtensions.cs
  24. 30 0
      Masuit.Tools.Core/AspNetCore/Extensions/ServiceCollectionExtensions.cs
  25. 18 0
      Masuit.Tools.Core/AspNetCore/ResumeFileResult/IResumeFileResult.cs
  26. 52 0
      Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumeFileContentResult.cs
  27. 53 0
      Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumeFileStreamResult.cs
  28. 52 0
      Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumePhysicalFileResult.cs
  29. 52 0
      Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumeVirtualFileResult.cs
  30. 6 5
      Masuit.Tools.Core/DateTimeExt/DateUtil.cs
  31. 6 2
      Masuit.Tools.Core/Masuit.Tools.Core.csproj
  32. 5 8
      Masuit.Tools.Core/Net/FtpClient.cs
  33. 11 40
      Masuit.Tools.Core/Systems/SnowFlake.cs
  34. 2 2
      Masuit.Tools.NoSQL.MongoDBClient/Masuit.Tools.NoSQL.MongoDBClient.csproj
  35. 1 1
      Masuit.Tools.NoSQL.MongoDBClient/packages.config
  36. 36 0
      Masuit.Tools.UnitTest/LinqExtensionTest.cs
  37. 1 0
      Masuit.Tools.UnitTest/Masuit.Tools.UnitTest.csproj
  38. 9 2
      Masuit.Tools.sln
  39. 2 0
      Masuit.Tools.sln.DotSettings
  40. 6 5
      Masuit.Tools/DateTimeExt/DateUtil.cs
  41. 2 2
      Masuit.Tools/Masuit.Tools.csproj
  42. 5 8
      Masuit.Tools/Net/FtpClient.cs
  43. 2 2
      Masuit.Tools/Properties/AssemblyInfo.cs
  44. 11 40
      Masuit.Tools/Systems/SnowFlake.cs
  45. 1 1
      Masuit.Tools/packages.config
  46. 0 1
      NetCoreTest/NetCoreTest.csproj
  47. 11 8
      Test/Program.cs
  48. 0 38
      Test/Test.csproj
  49. 0 1
      Test/packages.config

+ 139 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Controllers/TestController.cs

@@ -0,0 +1,139 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using System;
+using System.IO;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.Controllers
+{
+    /// <summary>
+    /// 测试控制器
+    /// </summary>
+    [Route("file")]
+    public class TestController : Controller
+    {
+        private const string EntityTag = "\"TestFile\"";
+
+        private readonly IHostingEnvironment _hostingEnvironment;
+
+        private readonly DateTimeOffset _lastModified = new DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan.Zero);
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="hostingEnvironment"></param>
+        public TestController(IHostingEnvironment hostingEnvironment)
+        {
+            _hostingEnvironment = hostingEnvironment;
+        }
+
+        [HttpGet("content/{fileName}/{etag}")]
+        public IActionResult FileContent(bool fileName, bool etag)
+        {
+            string webRoot = _hostingEnvironment.WebRootPath;
+            var content = System.IO.File.ReadAllBytes(Path.Combine(webRoot, "TestFile.txt"));
+            ResumeFileContentResult result = this.ResumeFile(content, "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
+            result.LastModified = _lastModified;
+            return result;
+        }
+
+        [HttpGet("content/{fileName}")]
+        public IActionResult FileContent(bool fileName)
+        {
+            string webRoot = _hostingEnvironment.WebRootPath;
+            var content = System.IO.File.ReadAllBytes(Path.Combine(webRoot, "TestFile.txt"));
+            var result = new ResumeFileContentResult(content, "text/plain")
+            {
+                FileInlineName = "TestFile.txt",
+                LastModified = _lastModified
+            };
+            return result;
+        }
+
+        [HttpHead("file")]
+        public IActionResult FileHead()
+        {
+            ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", "TestFile.txt", EntityTag);
+            result.LastModified = _lastModified;
+            return result;
+        }
+
+        [HttpPut("file")]
+        public IActionResult FilePut()
+        {
+            ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", "TestFile.txt", EntityTag);
+            result.LastModified = _lastModified;
+            return result;
+        }
+
+        [HttpGet("stream/{fileName}/{etag}")]
+        public IActionResult FileStream(bool fileName, bool etag)
+        {
+            string webRoot = _hostingEnvironment.WebRootPath;
+            FileStream stream = System.IO.File.OpenRead(Path.Combine(webRoot, "TestFile.txt"));
+
+            ResumeFileStreamResult result = this.ResumeFile(stream, "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
+            result.LastModified = _lastModified;
+            return result;
+        }
+
+        [HttpGet("stream/{fileName}")]
+        public IActionResult FileStream(bool fileName)
+        {
+            string webRoot = _hostingEnvironment.WebRootPath;
+            FileStream stream = System.IO.File.OpenRead(Path.Combine(webRoot, "TestFile.txt"));
+
+            var result = new ResumeFileStreamResult(stream, "text/plain")
+            {
+                FileInlineName = "TestFile.txt",
+                LastModified = _lastModified
+            };
+
+            return result;
+        }
+
+        [HttpGet("physical/{fileName}/{etag}")]
+        public IActionResult PhysicalFile(bool fileName, bool etag)
+        {
+            string webRoot = _hostingEnvironment.WebRootPath;
+
+            ResumePhysicalFileResult result = this.ResumePhysicalFile(Path.Combine(webRoot, "TestFile.txt"), "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
+            result.LastModified = _lastModified;
+            return result;
+        }
+
+        [HttpGet("physical/{fileName}")]
+        public IActionResult PhysicalFile(bool fileName)
+        {
+            string webRoot = _hostingEnvironment.WebRootPath;
+
+            var result = new ResumePhysicalFileResult(Path.Combine(webRoot, "TestFile.txt"), "text/plain")
+            {
+                FileInlineName = "TestFile.txt",
+                LastModified = _lastModified
+            };
+
+            return result;
+        }
+
+        [HttpGet("virtual/{fileName}/{etag}")]
+        public IActionResult VirtualFile(bool fileName, bool etag)
+        {
+            ResumeVirtualFileResult result = this.ResumeFile("TestFile.txt", "text/plain", fileName ? "TestFile.txt" : null, etag ? EntityTag : null);
+            result.LastModified = _lastModified;
+            return result;
+        }
+
+        [HttpGet("virtual/{fileName}")]
+        public IActionResult VirtualFile(bool fileName)
+        {
+            var result = new ResumeVirtualFileResult("TestFile.txt", "text/plain")
+            {
+                FileInlineName = "TestFile.txt",
+                LastModified = _lastModified
+            };
+            return result;
+        }
+    }
+}

+ 35 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj

@@ -0,0 +1,35 @@
+<Project Sdk="Microsoft.NET.Sdk.Web">
+
+  <PropertyGroup>
+    <TargetFramework>netcoreapp2.1</TargetFramework>
+    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <DocumentationFile>D:\Private\Masuit.Tools\Masuit.Tools.AspNetCore.ResumeFileResults.WebTest\Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.xml</DocumentationFile>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <Content Remove="wwwroot\TestFile.txt" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Include="wwwroot\TestFile.txt" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.App" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\Masuit.Tools.Core\Masuit.Tools.Core.csproj" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <None Update="Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.xml">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </None>
+  </ItemGroup>
+
+</Project>

+ 19 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.xml

@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<doc>
+    <assembly>
+        <name>Masuit.Tools.AspNetCore.ResumeFileResults.WebTest</name>
+    </assembly>
+    <members>
+        <member name="T:Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.Controllers.TestController">
+            <summary>
+            测试控制器
+            </summary>
+        </member>
+        <member name="M:Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.Controllers.TestController.#ctor(Microsoft.AspNetCore.Hosting.IHostingEnvironment)">
+            <summary>
+            
+            </summary>
+            <param name="hostingEnvironment"></param>
+        </member>
+    </members>
+</doc>

+ 24 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Program.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.WebTest
+{
+    public class Program
+    {
+        public static void Main(string[] args)
+        {
+            CreateWebHostBuilder(args).Build().Run();
+        }
+
+        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
+            WebHost.CreateDefaultBuilder(args)
+                .UseStartup<Startup>();
+    }
+}

+ 28 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Properties/launchSettings.json

@@ -0,0 +1,28 @@
+{
+  "iisSettings": {
+    "windowsAuthentication": false,
+    "anonymousAuthentication": true,
+    "iisExpress": {
+      "applicationUrl": "http://localhost:40356",
+      "sslPort": 0
+    }
+  },
+  "$schema": "http://json.schemastore.org/launchsettings.json",
+  "profiles": {
+    "IIS Express": {
+      "commandName": "IISExpress",
+      "launchUrl": "api/values",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "Masuit.Tools.AspNetCore.ResumeFileResults.WebTest": {
+      "commandName": "Project",
+      "launchUrl": "api/values",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      },
+      "applicationUrl": "http://localhost:5000"
+    }
+  }
+}

+ 65 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Startup.cs

@@ -0,0 +1,65 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.PlatformAbstractions;
+using Swashbuckle.AspNetCore.Swagger;
+using System.IO;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.WebTest
+{
+    public class Startup
+    {
+        public Startup(IConfiguration configuration)
+        {
+            Configuration = configuration;
+        }
+
+        public IConfiguration Configuration { get; }
+
+        public void ConfigureServices(IServiceCollection services)
+        {
+            services.AddResumeFileResult();
+            services.AddSwaggerGen(c =>
+            {
+                c.SwaggerDoc("v1", new Info
+                {
+                    Title = "API文档",
+                    Version = "v1",
+                    Contact = new Contact()
+                    {
+                        Email = "[email protected]",
+                        Name = "懒得勤快",
+                        Url = "https://masuit.com"
+                    },
+                    Description = "断点续传和多线程下载测试站点",
+                    License = new License()
+                    {
+                        Name = "懒得勤快",
+                        Url = "https://masuit.com"
+                    }
+                });
+                c.DescribeAllEnumsAsStrings();
+                var basePath = PlatformServices.Default.Application.ApplicationBasePath;
+                var xmlPath = Path.Combine(basePath, "Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.xml");
+                c.IncludeXmlComments(xmlPath);
+            });
+            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
+        }
+
+        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
+        {
+            if (env.IsDevelopment())
+            {
+                app.UseDeveloperExceptionPage();
+            }
+            app.UseSwagger().UseSwaggerUI(c =>
+            {
+                c.SwaggerEndpoint($"{Configuration["Swagger:VirtualPath"]}/swagger/v1/swagger.json", "断点续传和多线程下载测试站点");
+            });
+            app.UseMvcWithDefaultRoute();
+        }
+    }
+}

+ 9 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/appsettings.Development.json

@@ -0,0 +1,9 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Debug",
+      "System": "Information",
+      "Microsoft": "Information"
+    }
+  }
+}

+ 8 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/appsettings.json

@@ -0,0 +1,8 @@
+{
+  "Logging": {
+    "LogLevel": {
+      "Default": "Warning"
+    }
+  },
+  "AllowedHosts": "*"
+}

+ 1 - 0
Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/wwwroot/TestFile.txt

@@ -0,0 +1 @@
+0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ

+ 244 - 0
Masuit.Tools.Core.UnitTest/AspNetCore/FileContentsTests.cs

@@ -0,0 +1,244 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Masuit.Tools.Core.UnitTest.AspNetCore
+{
+    public class FileContentsTests : TestBase
+    {
+        [Fact]
+        public async Task FullFileContentsAttachmentEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileContentsAttachmentNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileContentsInlineEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileContentsInlineNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileContentsInlineFileNameTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+            Assert.Equal("TestFile.txt", response.Content.Headers.ContentDisposition.FileName);
+        }
+
+        [Fact]
+        public async Task Partial1FileContentsAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1FileContentsAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileContentsAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileContentsAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileContentsInlineEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileContentsInlineNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/content/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+    }
+}

+ 244 - 0
Masuit.Tools.Core.UnitTest/AspNetCore/FileStreamTests.cs

@@ -0,0 +1,244 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Masuit.Tools.Core.UnitTest.AspNetCore
+{
+    public class FileStreamTests : TestBase
+    {
+        [Fact]
+        public async Task FullFileStreamAttachmentEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileStreamAttachmentNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileStreamInlineEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullFileStreamInlineFileNameTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+            Assert.Equal("TestFile.txt", response.Content.Headers.ContentDisposition.FileName);
+        }
+
+        [Fact]
+        public async Task FullFileStreamInlineNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1FileStreamAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1FileStreamAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileStreamAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileStreamAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileStreamInlineEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2FileStreamInlineNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/stream/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+    }
+}

+ 244 - 0
Masuit.Tools.Core.UnitTest/AspNetCore/PhysicalFileTests.cs

@@ -0,0 +1,244 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Masuit.Tools.Core.UnitTest.AspNetCore
+{
+    public class PhysicalFileTests : TestBase
+    {
+        [Fact]
+        public async Task FullPhysicalFileAttachmentEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullPhysicalFileAttachmentNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullPhysicalFileInlineEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullPhysicalFileInlineFileNameTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+            Assert.Equal("TestFile.txt", response.Content.Headers.ContentDisposition.FileName);
+        }
+
+        [Fact]
+        public async Task FullPhysicalFileInlineNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1PhysicalFileAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1PhysicalFileAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2PhysicalFileAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2PhysicalFileAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2PhysicalFileInlineEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2PhysicalFileInlineNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+    }
+}

+ 648 - 0
Masuit.Tools.Core.UnitTest/AspNetCore/PreconditionTests.cs

@@ -0,0 +1,648 @@
+using Microsoft.Net.Http.Headers;
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Masuit.Tools.Core.UnitTest.AspNetCore
+{
+    public class PreconditionTests : TestBase
+    {
+        /// <summary>
+        /// The precondition if match fail test.
+        /// </summary>
+        [Fact]
+        public async Task PreconditionIfMatchFailTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Match", "\"xyzzy\"");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfMatchEmptySuccessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Match", "\"xyzzy\"");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/false");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfMatchFailWeakTest()
+        {
+            // Arrange
+            string entityTag = EntityTag.ToString();
+            var tmpNewEntityTag = new EntityTagHeaderValue(entityTag, true);
+            Client.DefaultRequestHeaders.Add("If-Match", tmpNewEntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfMatchSuccessAnyTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Match", EntityTagHeaderValue.Any.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfMatchSuccessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Match", EntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfModifiedSinceFailEqualTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Modified-Since", HeaderUtilities.FormatDate(LastModified));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfModifiedSinceFailLessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Modified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfModifiedSinceIfNoneMatchSuccessTest()
+        {
+            // Arrange
+            var tmpNewEntityTag = new EntityTagHeaderValue("\"xyzzy\"", true);
+            Client.DefaultRequestHeaders.Add("If-None-Match", tmpNewEntityTag.ToString());
+            Client.DefaultRequestHeaders.Add("If-Modified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfModifiedSinceSuccessLessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Modified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(-1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfModifiedSinceSuccessPutTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Modified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(-1)));
+
+            // Act
+            HttpResponseMessage response = await Client.PutAsync("/file/file", new StringContent(string.Empty));
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchFailAnyGetTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-None-Match", EntityTagHeaderValue.Any.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchFailAnyPutTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-None-Match", EntityTagHeaderValue.Any.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.PutAsync("/file/file", new StringContent(string.Empty));
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchGetFailTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-None-Match", EntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchGetWeakSuccessTest()
+        {
+            // Arrange
+            string entityTag = EntityTag.ToString();
+            var tmpNewEntityTag = new EntityTagHeaderValue(entityTag, true);
+            Client.DefaultRequestHeaders.Add("If-None-Match", tmpNewEntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchHeadFailTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-None-Match", EntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "/file/file"));
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchPutFailTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-None-Match", EntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.PutAsync("/file/file", new StringContent(string.Empty));
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.NotModified, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchSuccessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-None-Match", "\"xyzzy\"");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfNoneMatchSuccessWeakTest()
+        {
+            // Arrange
+            var tmpNewEntityTag = new EntityTagHeaderValue("\"xyzzy\"", true);
+            Client.DefaultRequestHeaders.Add("If-None-Match", tmpNewEntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", EntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeIgnoreEtagEmptyTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", EntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeIgnoreEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", "\"xyzzy\"");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeIgnoreTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Range", "\"xyzzy\"");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeIgnoreWeakEtagTest()
+        {
+            // Arrange
+            var tmpNewEntityTag = new EntityTagHeaderValue(EntityTag.ToString(), true);
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", tmpNewEntityTag.ToString());
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeLastModifiedEqualTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", HeaderUtilities.FormatDate(LastModified));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeLastModifiedIgnoreTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", HeaderUtilities.FormatDate(LastModified.AddSeconds(-1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfRangeLastModifiedLessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+            Client.DefaultRequestHeaders.Add("If-Range", HeaderUtilities.FormatDate(LastModified.AddSeconds(1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfUnmodifiedSinceFailTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Unmodified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(-1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfUnmodifiedSinceIfMatchFailTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Match", EntityTag.ToString());
+            Client.DefaultRequestHeaders.Add("If-Unmodified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(-1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PreconditionFailed, response.StatusCode);
+            Assert.Equal(string.Empty, responseString);
+            Assert.NotEqual("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfUnmodifiedSinceSuccessEqualTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Unmodified-Since", HeaderUtilities.FormatDate(LastModified));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task PreconditionIfUnmodifiedSinceSuccessLessTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("If-Unmodified-Since", HeaderUtilities.FormatDate(LastModified.AddSeconds(1)));
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/physical/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+    }
+}

+ 32 - 0
Masuit.Tools.Core.UnitTest/AspNetCore/TestBase.cs

@@ -0,0 +1,32 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.WebTest;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.TestHost;
+using System;
+using System.IO;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Reflection;
+
+namespace Masuit.Tools.Core.UnitTest.AspNetCore
+{
+    public abstract class TestBase
+    {
+        protected TestBase()
+        {
+            var path = Path.GetDirectoryName(typeof(Startup).GetTypeInfo().Assembly.Location);
+            var di = new DirectoryInfo(path).Parent.Parent.Parent;
+
+            // Arrange
+            Server = new TestServer(new WebHostBuilder().UseStartup<Startup>().UseContentRoot(di.FullName));
+            Client = Server.CreateClient();
+        }
+
+        public HttpClient Client { get; }
+
+        public EntityTagHeaderValue EntityTag { get; } = new EntityTagHeaderValue("\"TestFile\"");
+
+        public DateTimeOffset LastModified { get; } = new DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan.Zero);
+
+        public TestServer Server { get; }
+    }
+}

+ 246 - 0
Masuit.Tools.Core.UnitTest/AspNetCore/VirtualFileTests.cs

@@ -0,0 +1,246 @@
+using System.Net;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Masuit.Tools.Core.UnitTest.AspNetCore
+{
+    public class VirtualFileTests : TestBase
+    {
+        /// <summary>
+        /// The full virtual file attachment with entity tag test.
+        /// </summary>
+        [Fact]
+        public async Task FullVirtualFileAttachmentEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullVirtualFileAttachmentNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullVirtualFileInlineEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task FullVirtualFileInlineFileNameTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+            Assert.Equal("TestFile.txt", response.Content.Headers.ContentDisposition.FileName);
+        }
+
+        [Fact]
+        public async Task FullVirtualFileInlineNoEtagTest()
+        {
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.OK, response.StatusCode);
+            Assert.Equal("0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.Null(response.Content.Headers.ContentRange);
+            Assert.Equal(62, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1VirtualFileAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial1VirtualFileAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=0-0");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("0", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 0-0/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2VirtualFileAttachmentEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/true/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2VirtualFileAttachmentNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/true/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("attachment", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2VirtualFileInlineEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/false/true");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Equal(EntityTag, response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+
+        [Fact]
+        public async Task Partial2VirtualFileInlineNoEtagTest()
+        {
+            // Arrange
+            Client.DefaultRequestHeaders.Add("Range", "bytes=1-1");
+
+            // Act
+            HttpResponseMessage response = await Client.GetAsync("/file/virtual/false/false");
+            response.EnsureSuccessStatusCode();
+
+            string responseString = await response.Content.ReadAsStringAsync();
+
+            // Assert
+            Assert.Equal(HttpStatusCode.PartialContent, response.StatusCode);
+            Assert.Equal("1", responseString);
+            Assert.Equal("bytes", response.Headers.AcceptRanges.ToString());
+            Assert.Null(response.Headers.ETag);
+            Assert.NotNull(response.Content.Headers.ContentRange);
+            Assert.Equal("bytes 1-1/62", response.Content.Headers.ContentRange.ToString());
+            Assert.Equal(1, response.Content.Headers.ContentLength);
+            Assert.Equal("inline", response.Content.Headers.ContentDisposition.DispositionType);
+        }
+    }
+}

+ 2 - 0
Masuit.Tools.Core.UnitTest/Masuit.Tools.Core.UnitTest.csproj

@@ -7,6 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
+    <PackageReference Include="Microsoft.AspNetCore.TestHost" Version="2.2.0" />
     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
     <PackageReference Include="xunit" Version="2.4.1" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.1">
@@ -16,6 +17,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\Masuit.Tools.AspNetCore.ResumeFileResults.WebTest\Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj" />
     <ProjectReference Include="..\Masuit.Tools.Core\Masuit.Tools.Core.csproj" />
   </ItemGroup>
 

+ 1 - 0
Masuit.Tools.Core.UnitTest/wwwroot/TestFile.txt

@@ -0,0 +1 @@
+0123456789abcdefghijklmnopgrstuvwxyzABCDEFGHIJKLMNOPGRSTUVWXYZ

+ 46 - 0
Masuit.Tools.Core/AspNetCore/Executor/ResumeFileContentResultExecutor.cs

@@ -0,0 +1,46 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Executor
+{
+    /// <summary>
+    /// 断点续传文件FileResult执行器
+    /// </summary>
+    internal class ResumeFileContentResultExecutor : FileContentResultExecutor, IActionResultExecutor<ResumeFileContentResult>
+    {
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="loggerFactory"></param>
+        public ResumeFileContentResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
+        {
+        }
+
+        /// <summary>
+        /// 执行Result
+        /// </summary>
+        /// <param name="context"></param>
+        /// <param name="result"></param>
+        /// <returns></returns>
+        public virtual Task ExecuteAsync(ActionContext context, ResumeFileContentResult result)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+            context.SetContentDispositionHeaderInline(result);
+            return base.ExecuteAsync(context, result);
+        }
+    }
+}

+ 47 - 0
Masuit.Tools.Core/AspNetCore/Executor/ResumeFileStreamResultExecutor.cs

@@ -0,0 +1,47 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Executor
+{
+    /// <summary>
+    /// 可断点续传的FileStreamResult执行器
+    /// </summary>
+    internal class ResumeFileStreamResultExecutor : FileStreamResultExecutor, IActionResultExecutor<ResumeFileStreamResult>
+    {
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="loggerFactory"></param>
+        public ResumeFileStreamResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
+        {
+        }
+
+        /// <summary>
+        /// 执行Result
+        /// </summary>
+        /// <param name="context"></param>
+        /// <param name="result"></param>
+        /// <returns></returns>
+        public virtual Task ExecuteAsync(ActionContext context, ResumeFileStreamResult result)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+            context.SetContentDispositionHeaderInline(result);
+
+            return base.ExecuteAsync(context, result);
+        }
+    }
+}

+ 46 - 0
Masuit.Tools.Core/AspNetCore/Executor/ResumePhysicalFileResultExecutor.cs

@@ -0,0 +1,46 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Executor
+{
+    /// <summary>
+    /// 通过本地文件的可断点续传的FileResult执行器
+    /// </summary>
+    internal class ResumePhysicalFileResultExecutor : PhysicalFileResultExecutor, IActionResultExecutor<ResumePhysicalFileResult>
+    {
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="loggerFactory"></param>
+        public ResumePhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory)
+        {
+        }
+
+        /// <summary>
+        /// 执行Result
+        /// </summary>
+        /// <param name="context"></param>
+        /// <param name="result"></param>
+        /// <returns></returns>
+        public virtual Task ExecuteAsync(ActionContext context, ResumePhysicalFileResult result)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+            context.SetContentDispositionHeaderInline(result);
+            return base.ExecuteAsync(context, result);
+        }
+    }
+}

+ 49 - 0
Masuit.Tools.Core/AspNetCore/Executor/ResumeVirtualFileResultExecutor.cs

@@ -0,0 +1,49 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Extensions;
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Executor
+{
+    /// <summary>
+    /// 使用本地虚拟路径的可断点续传的FileResult
+    /// </summary>
+    internal class ResumeVirtualFileResultExecutor : VirtualFileResultExecutor, IActionResultExecutor<ResumeVirtualFileResult>
+    {
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="loggerFactory"></param>
+        /// <param name="hostingEnvironment"></param>
+        public ResumeVirtualFileResultExecutor(ILoggerFactory loggerFactory, IHostingEnvironment hostingEnvironment) : base(loggerFactory, hostingEnvironment)
+        {
+        }
+
+        /// <summary>
+        /// 执行FileResult
+        /// </summary>
+        /// <param name="context"></param>
+        /// <param name="result"></param>
+        /// <returns></returns>
+        public virtual Task ExecuteAsync(ActionContext context, ResumeVirtualFileResult result)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            if (result == null)
+            {
+                throw new ArgumentNullException(nameof(result));
+            }
+
+            context.SetContentDispositionHeaderInline(result);
+
+            return base.ExecuteAsync(context, result);
+        }
+    }
+}

+ 32 - 0
Masuit.Tools.Core/AspNetCore/Extensions/ActionContextExtension.cs

@@ -0,0 +1,32 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Net.Http.Headers;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Extensions
+{
+    /// <summary>
+    /// ResumeFileHelper
+    /// </summary>
+    public static class ActionContextExtension
+    {
+        /// <summary>
+        /// 设置响应头ContentDispositionHeader
+        /// </summary>
+        /// <param name="context"></param>
+        /// <param name="result"></param>
+        public static void SetContentDispositionHeaderInline(this ActionContext context, IResumeFileResult result)
+        {
+            if (string.IsNullOrEmpty(result.FileDownloadName))
+            {
+                var contentDisposition = new ContentDispositionHeaderValue("inline");
+
+                if (!string.IsNullOrWhiteSpace(result.FileInlineName))
+                {
+                    contentDisposition.SetHttpFileName(result.FileInlineName);
+                }
+
+                context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = contentDisposition.ToString();
+            }
+        }
+    }
+}

+ 180 - 0
Masuit.Tools.Core/AspNetCore/Extensions/ControllerExtensions.cs

@@ -0,0 +1,180 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Mvc;
+using System.IO;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Extensions
+{
+    /// <summary>
+    /// Controller扩展方法
+    /// </summary>
+    public static class ControllerExtensions
+    {
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="fileContents">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <returns></returns>
+        public static ResumeFileContentResult ResumeFile(this ControllerBase controller, byte[] fileContents, string contentType)
+        {
+            return ResumeFile(controller, fileContents, contentType, fileDownloadName: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="fileContents">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <returns></returns>
+        public static ResumeFileContentResult ResumeFile(this ControllerBase controller, byte[] fileContents, string contentType, string fileDownloadName)
+        {
+            return ResumeFile(controller, fileContents, contentType, fileDownloadName, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="fileContents">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <param name="etag">ETag</param>
+        /// <returns></returns>
+        public static ResumeFileContentResult ResumeFile(this ControllerBase controller, byte[] fileContents, string contentType, string fileDownloadName, string etag)
+        {
+            return new ResumeFileContentResult(fileContents, contentType, etag)
+            {
+                FileDownloadName = fileDownloadName
+            };
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="fileStream">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <returns></returns>
+        public static ResumeFileStreamResult ResumeFile(this ControllerBase controller, Stream fileStream, string contentType)
+        {
+            return ResumeFile(controller, fileStream, contentType, fileDownloadName: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="fileStream">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <returns></returns>
+        public static ResumeFileStreamResult ResumeFile(this ControllerBase controller, Stream fileStream, string contentType, string fileDownloadName)
+        {
+            return ResumeFile(controller, fileStream, contentType, fileDownloadName, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="fileStream">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <param name="etag">ETag</param>
+        /// <returns></returns>
+        public static ResumeFileStreamResult ResumeFile(this ControllerBase controller, Stream fileStream, string contentType, string fileDownloadName, string etag)
+        {
+            return new ResumeFileStreamResult(fileStream, contentType, etag)
+            {
+                FileDownloadName = fileDownloadName
+            };
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="virtualPath">服务端本地文件的虚拟路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <returns></returns>
+        public static ResumeVirtualFileResult ResumeFile(this ControllerBase controller, string virtualPath, string contentType)
+        {
+            return ResumeFile(controller, virtualPath, contentType, fileDownloadName: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="virtualPath">服务端本地文件的虚拟路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <returns></returns>
+        public static ResumeVirtualFileResult ResumeFile(this ControllerBase controller, string virtualPath, string contentType, string fileDownloadName)
+        {
+            return ResumeFile(controller, virtualPath, contentType, fileDownloadName, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="virtualPath">服务端本地文件的虚拟路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <param name="etag">ETag</param>
+        /// <returns></returns>
+        public static ResumeVirtualFileResult ResumeFile(this ControllerBase controller, string virtualPath, string contentType, string fileDownloadName, string etag)
+        {
+            return new ResumeVirtualFileResult(virtualPath, contentType, etag)
+            {
+                FileDownloadName = fileDownloadName
+            };
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="physicalPath">服务端本地文件的物理路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <returns></returns>
+        public static ResumePhysicalFileResult ResumePhysicalFile(this ControllerBase controller, string physicalPath, string contentType)
+        {
+            return ResumePhysicalFile(controller, physicalPath, contentType, fileDownloadName: null, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="physicalPath">服务端本地文件的物理路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <returns></returns>
+        public static ResumePhysicalFileResult ResumePhysicalFile(this ControllerBase controller, string physicalPath, string contentType, string fileDownloadName)
+        {
+            return ResumePhysicalFile(controller, physicalPath, contentType, fileDownloadName, etag: null);
+        }
+
+        /// <summary>
+        /// 可断点续传和多线程下载的FileResult
+        /// </summary>
+        /// <param name="controller"></param>
+        /// <param name="physicalPath">服务端本地文件的物理路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="fileDownloadName">下载的文件名</param>
+        /// <param name="etag">ETag</param>
+        /// <returns></returns>
+        public static ResumePhysicalFileResult ResumePhysicalFile(this ControllerBase controller, string physicalPath, string contentType, string fileDownloadName, string etag)
+        {
+            return new ResumePhysicalFileResult(physicalPath, contentType, etag)
+            {
+                FileDownloadName = fileDownloadName
+            };
+        }
+    }
+}

+ 30 - 0
Masuit.Tools.Core/AspNetCore/Extensions/ServiceCollectionExtensions.cs

@@ -0,0 +1,30 @@
+using Masuit.Tools.AspNetCore.ResumeFileResults.Executor;
+using Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using System;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.Extensions
+{
+    /// <summary>
+    /// 依赖注入ServiceCollection容器扩展方法
+    /// </summary>
+    public static class ServiceCollectionExtensions
+    {
+        /// <summary>
+        /// 注入断点续传服务
+        /// </summary>
+        /// <param name="services"></param>
+        /// <returns></returns>
+        /// <exception cref="ArgumentNullException"></exception>
+        public static IServiceCollection AddResumeFileResult(this IServiceCollection services)
+        {
+            services.TryAddSingleton<IActionResultExecutor<ResumePhysicalFileResult>, ResumePhysicalFileResultExecutor>();
+            services.TryAddSingleton<IActionResultExecutor<ResumeVirtualFileResult>, ResumeVirtualFileResultExecutor>();
+            services.TryAddSingleton<IActionResultExecutor<ResumeFileStreamResult>, ResumeFileStreamResultExecutor>();
+            services.TryAddSingleton<IActionResultExecutor<ResumeFileContentResult>, ResumeFileContentResultExecutor>();
+            return services;
+        }
+    }
+}

+ 18 - 0
Masuit.Tools.Core/AspNetCore/ResumeFileResult/IResumeFileResult.cs

@@ -0,0 +1,18 @@
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult
+{
+    /// <summary>
+    /// 可断点续传的FileResult
+    /// </summary>
+    public interface IResumeFileResult
+    {
+        /// <summary>
+        /// 文件下载名
+        /// </summary>
+        string FileDownloadName { get; set; }
+
+        /// <summary>
+        /// 给响应头的文件名
+        /// </summary>
+        string FileInlineName { get; set; }
+    }
+}

+ 52 - 0
Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumeFileContentResult.cs

@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult
+{
+    /// <summary>
+    /// 基于Stream的ResumeFileContentResult
+    /// </summary>
+    public class ResumeFileContentResult : FileContentResult, IResumeFileResult
+    {
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="fileContents">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumeFileContentResult(byte[] fileContents, string contentType, string etag = null) : this(fileContents, MediaTypeHeaderValue.Parse(contentType), !string.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null)
+        {
+        }
+
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="fileContents">文件二进制流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumeFileContentResult(byte[] fileContents, MediaTypeHeaderValue contentType, EntityTagHeaderValue etag = null) : base(fileContents, contentType)
+        {
+            EntityTag = etag;
+            EnableRangeProcessing = true;
+        }
+
+        /// <inheritdoc/>
+        public string FileInlineName { get; set; }
+
+        /// <inheritdoc/>
+        public override Task ExecuteResultAsync(ActionContext context)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ResumeFileContentResult>>();
+            return executor.ExecuteAsync(context, this);
+        }
+    }
+}

+ 53 - 0
Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumeFileStreamResult.cs

@@ -0,0 +1,53 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult
+{
+    /// <summary>
+    /// 基于Stream的ResumeFileStreamResult
+    /// </summary>
+    public class ResumeFileStreamResult : FileStreamResult, IResumeFileResult
+    {
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="fileStream">文件流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumeFileStreamResult(Stream fileStream, string contentType, string etag = null) : this(fileStream, MediaTypeHeaderValue.Parse(contentType), !string.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null)
+        {
+        }
+
+        /// <summary>
+        /// 构造函数
+        /// </summary>
+        /// <param name="fileStream">文件流</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumeFileStreamResult(Stream fileStream, MediaTypeHeaderValue contentType, EntityTagHeaderValue etag = null) : base(fileStream, contentType)
+        {
+            EntityTag = etag;
+            EnableRangeProcessing = true;
+        }
+
+        /// <inheritdoc/>
+        public string FileInlineName { get; set; }
+
+        /// <inheritdoc/>
+        public override Task ExecuteResultAsync(ActionContext context)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ResumeFileStreamResult>>();
+            return executor.ExecuteAsync(context, this);
+        }
+    }
+}

+ 52 - 0
Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumePhysicalFileResult.cs

@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult
+{
+    /// <summary>
+    /// 基于本地物理路径的ResumePhysicalFileResult
+    /// </summary>
+    public class ResumePhysicalFileResult : PhysicalFileResult, IResumeFileResult
+    {
+        /// <summary>
+        /// 基于本地物理路径的ResumePhysicalFileResult
+        /// </summary>
+        /// <param name="fileName">文件全路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumePhysicalFileResult(string fileName, string contentType, string etag = null) : this(fileName, MediaTypeHeaderValue.Parse(contentType), !string.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null)
+        {
+        }
+
+        /// <summary>
+        /// 基于本地物理路径的ResumePhysicalFileResult
+        /// </summary>
+        /// <param name="fileName">文件全路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumePhysicalFileResult(string fileName, MediaTypeHeaderValue contentType, EntityTagHeaderValue etag = null) : base(fileName, contentType)
+        {
+            EntityTag = etag;
+            EnableRangeProcessing = true;
+        }
+
+        /// <inheritdoc/>
+        public string FileInlineName { get; set; }
+
+        /// <inheritdoc/>
+        public override Task ExecuteResultAsync(ActionContext context)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ResumePhysicalFileResult>>();
+            return executor.ExecuteAsync(context, this);
+        }
+    }
+}

+ 52 - 0
Masuit.Tools.Core/AspNetCore/ResumeFileResult/ResumeVirtualFileResult.cs

@@ -0,0 +1,52 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+using System;
+using System.Threading.Tasks;
+
+namespace Masuit.Tools.AspNetCore.ResumeFileResults.ResumeFileResult
+{
+    /// <summary>
+    /// 基于服务器虚拟路径路径的ResumePhysicalFileResult
+    /// </summary>
+    public class ResumeVirtualFileResult : VirtualFileResult, IResumeFileResult
+    {
+        /// <summary>
+        /// 基于服务器虚拟路径路径的ResumePhysicalFileResult
+        /// </summary>
+        /// <param name="fileName">文件全路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumeVirtualFileResult(string fileName, string contentType, string etag = null) : this(fileName, MediaTypeHeaderValue.Parse(contentType), !string.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null)
+        {
+        }
+
+        /// <summary>
+        /// 基于服务器虚拟路径路径的ResumePhysicalFileResult
+        /// </summary>
+        /// <param name="fileName">文件全路径</param>
+        /// <param name="contentType">Content-Type</param>
+        /// <param name="etag">ETag</param>
+        public ResumeVirtualFileResult(string fileName, MediaTypeHeaderValue contentType, EntityTagHeaderValue etag = null) : base(fileName, contentType)
+        {
+            EntityTag = etag;
+            EnableRangeProcessing = true;
+        }
+
+        /// <inheritdoc/>
+        public string FileInlineName { get; set; }
+
+        /// <inheritdoc/>
+        public override Task ExecuteResultAsync(ActionContext context)
+        {
+            if (context == null)
+            {
+                throw new ArgumentNullException(nameof(context));
+            }
+
+            var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<ResumeVirtualFileResult>>();
+            return executor.ExecuteAsync(context, this);
+        }
+    }
+}

+ 6 - 5
Masuit.Tools.Core/DateTimeExt/DateUtil.cs

@@ -7,6 +7,7 @@ namespace Masuit.Tools.DateTimeExt
     /// </summary>
     public static class DateUtil
     {
+        private static readonly DateTime Start1970 = DateTime.Parse("1970-01-01 00:00:00");
         /// <summary>
         /// 返回相对于当前时间的相对天数
         /// </summary>
@@ -52,35 +53,35 @@ namespace Masuit.Tools.DateTimeExt
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalSeconds(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalSeconds;
+        public static double GetTotalSeconds(this DateTime dt) => (dt - Start1970).TotalSeconds;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的毫秒数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalMilliseconds(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalMilliseconds;
+        public static double GetTotalMilliseconds(this DateTime dt) => (dt - Start1970).TotalMilliseconds;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的分钟数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalMinutes(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalMinutes;
+        public static double GetTotalMinutes(this DateTime dt) => (dt - Start1970).TotalMinutes;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的小时数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalHours(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalHours;
+        public static double GetTotalHours(this DateTime dt) => (dt - Start1970).TotalHours;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的天数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalDays(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalDays;
+        public static double GetTotalDays(this DateTime dt) => (dt - Start1970).TotalDays;
 
         /// <summary>
         /// 返回本年有多少天

+ 6 - 2
Masuit.Tools.Core/Masuit.Tools.Core.csproj

@@ -2,7 +2,7 @@
 
   <PropertyGroup>
     <TargetFramework>netcoreapp2.1</TargetFramework>
-    <Version>2.1.6</Version>
+    <Version>2.2</Version>
     <Authors>懒得勤快</Authors>
     <Company>masuit.com</Company>
     <Description>包含一些常用的操作类,大都是静态类,加密解密,反射操作,硬件信息,字符串扩展方法,日期时间扩展操作,大文件拷贝,图像裁剪,html处理,验证码、NoSql等常用封装。
@@ -90,8 +90,12 @@ string s = html.HtmlSantinizerStandard();//清理后:&lt;div&gt;&lt;span&gt;&l
     <PackageReference Include="HtmlSanitizer" Version="4.0.199" />
     <PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.5" />
     <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
-    <PackageReference Include="SharpZipLib" Version="1.0.0" />
+    <PackageReference Include="SharpZipLib" Version="1.1.0" />
     <PackageReference Include="System.Drawing.Common" Version="4.5.1" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Folder Include="AspNetCore\" />
+  </ItemGroup>
+
 </Project>

+ 5 - 8
Masuit.Tools.Core/Net/FtpClient.cs

@@ -424,9 +424,9 @@ namespace Masuit.Tools.Net
         /// 获取当前目录下明细(包含文件和文件夹)
         /// </summary>
         /// <returns></returns>
-        public string[] GetFilesDetails(string relativePath = "")
+        public List<string> GetFilesDetails(string relativePath = "")
         {
-            StringBuilder result = new StringBuilder();
+            List<string> result = new List<string>();
             var ftp = (FtpWebRequest)WebRequest.Create(new Uri(Path.Combine("ftp://" + FtpServer, relativePath).Replace("\\", "/")));
             ftp.Credentials = new NetworkCredential(Username, Password);
             ftp.Method = WebRequestMethods.Ftp.ListDirectoryDetails;
@@ -437,16 +437,13 @@ namespace Masuit.Tools.Net
                     string line = reader.ReadLine();
                     while (line != null)
                     {
-                        result.Append(line);
-                        result.Append("\n");
+                        result.Add(line);
                         line = reader.ReadLine();
                     }
-
-                    result.Remove(result.ToString().LastIndexOf("\n", StringComparison.Ordinal), 1);
                 }
             }
 
-            return result.ToString().Split('\n');
+            return result;
         }
 
         /// <summary>
@@ -494,7 +491,7 @@ namespace Masuit.Tools.Net
         /// <returns></returns>
         public string[] GetDirectories(string relativePath)
         {
-            string[] drectory = GetFilesDetails(relativePath);
+            var drectory = GetFilesDetails(relativePath);
             string m = string.Empty;
             foreach (string str in drectory)
             {

+ 11 - 40
Masuit.Tools.Core/Systems/SnowFlake.cs

@@ -1,4 +1,5 @@
-using Masuit.Tools.Strings;
+using Masuit.Tools.DateTimeExt;
+using Masuit.Tools.Strings;
 using System;
 
 namespace Masuit.Tools.Systems
@@ -13,6 +14,7 @@ namespace Masuit.Tools.Systems
         private static long _machineId; //机器码
         private static long _datacenterId; //数据ID
         private static long _sequence; //计数从零开始
+        private static long _lastTimestamp = -1L; //最后时间戳
 
         private const long Twepoch = 687888001020L; //唯一时间随机量
 
@@ -25,8 +27,7 @@ namespace Masuit.Tools.Systems
         private const long MachineIdShift = SequenceBits; //机器码数据左移位数,就是后面计数器占用的位数
         private const long DatacenterIdShift = SequenceBits + MachineIdBits;
         private const long TimestampLeftShift = DatacenterIdShift + DatacenterIdBits; //时间戳左移动位数就是机器码+计数器总字节数+数据字节数
-        private const long SequenceMask = -1L ^ -1L << (int)SequenceBits; //一微秒内可以产生计数,如果达到该值则等到下一微秒在进行生成
-        private static long _lastTimestamp = -1L; //最后时间戳
+        private const long SequenceMask = -1L ^ -1L << (int)SequenceBits; //一毫秒内可以产生计数,如果达到该值则等到下一毫秒在进行生成
 
         private static readonly object SyncRoot = new object(); //加锁对象
         private static readonly NumberFormater NumberFormater = new NumberFormater(36);
@@ -48,7 +49,7 @@ namespace Masuit.Tools.Systems
         /// </summary>
         public SnowFlake()
         {
-            Snowflakes(0L, -1);
+            Snowflakes(0, -1);
         }
 
         /// <summary>
@@ -93,31 +94,6 @@ namespace Masuit.Tools.Systems
             }
         }
 
-        /// <summary>
-        /// 生成当前时间戳
-        /// </summary>
-        /// <returns>毫秒</returns>
-        private static long GetTimestamp()
-        {
-            return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
-        }
-
-        /// <summary>
-        /// 获取下一微秒时间戳
-        /// </summary>
-        /// <param name="lastTimestamp"></param>
-        /// <returns></returns>
-        private static long GetNextTimestamp(long lastTimestamp)
-        {
-            long timestamp = GetTimestamp();
-            if (timestamp <= lastTimestamp)
-            {
-                timestamp = GetTimestamp();
-            }
-
-            return timestamp;
-        }
-
         /// <summary>
         /// 获取长整形的ID
         /// </summary>
@@ -126,28 +102,23 @@ namespace Masuit.Tools.Systems
         {
             lock (SyncRoot)
             {
-                long timestamp = GetTimestamp();
+                var timestamp = (long)DateTime.UtcNow.GetTotalMilliseconds();
                 if (_lastTimestamp == timestamp)
                 {
-                    //同一秒中生成ID
-                    _sequence = (_sequence + 1) & SequenceMask; //用&运算计算该秒内产生的计数是否已经到达上限
+                    //同一秒中生成ID
+                    _sequence = (_sequence + 1) & SequenceMask; //用&运算计算该秒内产生的计数是否已经到达上限
                     if (_sequence == 0)
                     {
-                        //一微秒内产生的ID计数已达上限,等待下一微
-                        timestamp = GetNextTimestamp(_lastTimestamp);
+                        //一毫秒内产生的ID计数已达上限,等待下一毫
+                        timestamp = (long)DateTime.UtcNow.GetTotalMilliseconds();
                     }
                 }
                 else
                 {
-                    //不同秒生成ID
+                    //不同秒生成ID
                     _sequence = 0L;
                 }
 
-                if (timestamp < _lastTimestamp)
-                {
-                    throw new Exception("时间戳比上一次生成ID时时间戳还小,故异常");
-                }
-
                 _lastTimestamp = timestamp; //把当前时间戳保存为最后生成ID的时间戳
                 long id = ((timestamp - Twepoch) << (int)TimestampLeftShift) | (_datacenterId << (int)DatacenterIdShift) | (_machineId << (int)MachineIdShift) | _sequence;
                 return id;

+ 2 - 2
Masuit.Tools.NoSQL.MongoDBClient/Masuit.Tools.NoSQL.MongoDBClient.csproj

@@ -40,8 +40,8 @@
     <Reference Include="HtmlSanitizer, Version=3.0.0.0, Culture=neutral, PublicKeyToken=61c49a1a9e79cc28, processorArchitecture=MSIL">
       <HintPath>..\packages\HtmlSanitizer.4.0.199\lib\net45\HtmlSanitizer.dll</HintPath>
     </Reference>
-    <Reference Include="ICSharpCode.SharpZipLib, Version=1.0.0.999, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
-      <HintPath>..\packages\SharpZipLib.1.0.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
+    <Reference Include="ICSharpCode.SharpZipLib, Version=1.1.0.145, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
+      <HintPath>..\packages\SharpZipLib.1.1.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
     </Reference>
     <Reference Include="Microsoft.CSharp" />
     <Reference Include="MongoDB.Bson, Version=2.7.2.0, Culture=neutral, processorArchitecture=MSIL">

+ 1 - 1
Masuit.Tools.NoSQL.MongoDBClient/packages.config

@@ -7,7 +7,7 @@
   <package id="MongoDB.Driver" version="2.7.2" targetFramework="net45" />
   <package id="MongoDB.Driver.Core" version="2.7.2" targetFramework="net45" />
   <package id="Newtonsoft.Json" version="12.0.1" targetFramework="net45" />
-  <package id="SharpZipLib" version="1.0.0" targetFramework="net45" />
+  <package id="SharpZipLib" version="1.1.0" targetFramework="net45" />
   <package id="StackExchange.Redis" version="1.2.6" targetFramework="net45" />
   <package id="System.Buffers" version="4.4.0" targetFramework="net45" />
   <package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net45" />

+ 36 - 0
Masuit.Tools.UnitTest/LinqExtensionTest.cs

@@ -0,0 +1,36 @@
+using Masuit.Tools.Core.Linq;
+using System;
+using System.Linq.Expressions;
+using Xunit;
+
+namespace Masuit.Tools.UnitTest
+{
+    public class LinqExtensionTest
+    {
+        [Fact]
+        public void And_TwoBoolExpression()
+        {
+            //arrange
+            Expression<Func<string, bool>> where1 = s => s.StartsWith("a");
+            Expression<Func<string, bool>> where2 = s => s.Length > 10;
+            Func<string, bool> func = where1.And(where2).Compile();
+
+            //act assert
+            Assert.False(func("abc"));
+            Assert.True(func("abcd12345678"));
+        }
+        [Fact]
+        public void Or_TwoBoolExpression()
+        {
+            //arrange
+            Expression<Func<string, bool>> where1 = s => s.StartsWith("a");
+            Expression<Func<string, bool>> where2 = s => s.Length > 10;
+            Func<string, bool> func = where1.Or(where2).Compile();
+
+            //act assert
+            Assert.True(func("abc"));
+            Assert.True(func("abcd12345678"));
+            Assert.False(func("cbcd12348"));
+        }
+    }
+}

+ 1 - 0
Masuit.Tools.UnitTest/Masuit.Tools.UnitTest.csproj

@@ -66,6 +66,7 @@
     <Compile Include="ChineseCalendarTest.cs" />
     <Compile Include="ExtensionMethodsTest.cs" />
     <Compile Include="HtmlToolsTest.cs" />
+    <Compile Include="LinqExtensionTest.cs" />
     <Compile Include="NumberFormaterTest.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="TemplateTest.cs" />

+ 9 - 2
Masuit.Tools.sln

@@ -13,18 +13,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masuit.Tools.NoSQL.MongoDBC
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masuit.Tools.UnitTest", "Masuit.Tools.UnitTest\Masuit.Tools.UnitTest.csproj", "{174474CB-0C9A-4C99-B461-1A2158F00A98}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masuit.Tools.Core.UnitTest", "Masuit.Tools.Core.UnitTest\Masuit.Tools.Core.UnitTest.csproj", "{82383331-259E-4FB8-BA90-47C0E1AFB4FA}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masuit.Tools.Core.UnitTest", "Masuit.Tools.Core.UnitTest\Masuit.Tools.Core.UnitTest.csproj", "{82383331-259E-4FB8-BA90-47C0E1AFB4FA}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masuit.Tools.NoSQL.MongoDBClient.UnitTest", "Masuit.Tools.NoSQL.MongoDBClient.UnitTest\Masuit.Tools.NoSQL.MongoDBClient.UnitTest.csproj", "{6CA28B75-2F2D-401A-BDF7-EE314C14C95F}"
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "测试", "测试", "{E0B8FBD1-A28A-4420-9DE2-6BD06035CBAC}"
 EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest", "Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest\Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest.csproj", "{9D2E3A00-9E94-4C3D-AAAE-70B0DA236FE6}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest", "Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest\Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest.csproj", "{9D2E3A00-9E94-4C3D-AAAE-70B0DA236FE6}"
 EndProject
 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Test", "Test\Test.csproj", "{91954D82-A5FC-417B-8FD9-D9C165C8F16C}"
 EndProject
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetCoreTest", "NetCoreTest\NetCoreTest.csproj", "{ABEC3F65-6556-44FD-BECE-33F7927FB3AE}"
 EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Masuit.Tools.AspNetCore.ResumeFileResults.WebTest", "Masuit.Tools.AspNetCore.ResumeFileResults.WebTest\Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj", "{49E16577-01CD-4104-B73E-449B2960D956}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
@@ -71,6 +73,10 @@ Global
 		{ABEC3F65-6556-44FD-BECE-33F7927FB3AE}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{ABEC3F65-6556-44FD-BECE-33F7927FB3AE}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{ABEC3F65-6556-44FD-BECE-33F7927FB3AE}.Release|Any CPU.Build.0 = Release|Any CPU
+		{49E16577-01CD-4104-B73E-449B2960D956}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{49E16577-01CD-4104-B73E-449B2960D956}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{49E16577-01CD-4104-B73E-449B2960D956}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{49E16577-01CD-4104-B73E-449B2960D956}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -80,6 +86,7 @@ Global
 		{82383331-259E-4FB8-BA90-47C0E1AFB4FA} = {E0B8FBD1-A28A-4420-9DE2-6BD06035CBAC}
 		{6CA28B75-2F2D-401A-BDF7-EE314C14C95F} = {E0B8FBD1-A28A-4420-9DE2-6BD06035CBAC}
 		{9D2E3A00-9E94-4C3D-AAAE-70B0DA236FE6} = {E0B8FBD1-A28A-4420-9DE2-6BD06035CBAC}
+		{49E16577-01CD-4104-B73E-449B2960D956} = {E0B8FBD1-A28A-4420-9DE2-6BD06035CBAC}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {B57FDA8F-95CF-478B-A0A8-7FF0F01CCFAB}

+ 2 - 0
Masuit.Tools.sln.DotSettings

@@ -0,0 +1,2 @@
+<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
+	<s:String x:Key="/Default/FilterSettingsManager/CoverageFilterXml/@EntryValue">&lt;data&gt;&lt;IncludeFilters /&gt;&lt;ExcludeFilters&gt;&lt;Filter ModuleMask="NetCoreTest" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;Filter ModuleMask="Test" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;Filter ModuleMask="Masuit.Tools.UnitTest" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;Filter ModuleMask="Masuit.Tools.NoSQL.MongoDBClient.Core.UnitTest" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;Filter ModuleMask="Masuit.Tools.Core.UnitTest" ModuleVersionMask="*" ClassMask="*" FunctionMask="*" IsEnabled="True" /&gt;&lt;/ExcludeFilters&gt;&lt;/data&gt;</s:String></wpf:ResourceDictionary>

+ 6 - 5
Masuit.Tools/DateTimeExt/DateUtil.cs

@@ -7,6 +7,7 @@ namespace Masuit.Tools.DateTimeExt
     /// </summary>
     public static class DateUtil
     {
+        private static readonly DateTime Start1970 = DateTime.Parse("1970-01-01 00:00:00");
         /// <summary>
         /// 返回相对于当前时间的相对天数
         /// </summary>
@@ -52,35 +53,35 @@ namespace Masuit.Tools.DateTimeExt
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalSeconds(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalSeconds;
+        public static double GetTotalSeconds(this DateTime dt) => (dt - Start1970).TotalSeconds;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的毫秒数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalMilliseconds(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalMilliseconds;
+        public static double GetTotalMilliseconds(this DateTime dt) => (dt - Start1970).TotalMilliseconds;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的分钟数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalMinutes(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalMinutes;
+        public static double GetTotalMinutes(this DateTime dt) => (dt - Start1970).TotalMinutes;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的小时数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalHours(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalHours;
+        public static double GetTotalHours(this DateTime dt) => (dt - Start1970).TotalHours;
 
         /// <summary>
         /// 获取该时间相对于1970-01-01 00:00:00的天数
         /// </summary>
         /// <param name="dt"></param>
         /// <returns></returns>
-        public static double GetTotalDays(this DateTime dt) => (dt - DateTime.Parse("1970-01-01 00:00:00")).TotalDays;
+        public static double GetTotalDays(this DateTime dt) => (dt - Start1970).TotalDays;
 
         /// <summary>
         /// 返回本年有多少天

+ 2 - 2
Masuit.Tools/Masuit.Tools.csproj

@@ -44,8 +44,8 @@
     <Reference Include="HtmlSanitizer, Version=3.0.0.0, Culture=neutral, PublicKeyToken=61c49a1a9e79cc28, processorArchitecture=MSIL">
       <HintPath>..\packages\HtmlSanitizer.4.0.199\lib\net45\HtmlSanitizer.dll</HintPath>
     </Reference>
-    <Reference Include="ICSharpCode.SharpZipLib, Version=1.0.0.999, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
-      <HintPath>..\packages\SharpZipLib.1.0.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
+    <Reference Include="ICSharpCode.SharpZipLib, Version=1.1.0.145, Culture=neutral, PublicKeyToken=1b03e6acf1164f73, processorArchitecture=MSIL">
+      <HintPath>..\packages\SharpZipLib.1.1.0\lib\net45\ICSharpCode.SharpZipLib.dll</HintPath>
     </Reference>
     <Reference Include="Microsoft.CSharp" />
     <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">

+ 5 - 8
Masuit.Tools/Net/FtpClient.cs

@@ -424,9 +424,9 @@ namespace Masuit.Tools.Net
         /// 获取当前目录下明细(包含文件和文件夹)
         /// </summary>
         /// <returns></returns>
-        public string[] GetFilesDetails(string relativePath = "")
+        public List<string> GetFilesDetails(string relativePath = "")
         {
-            StringBuilder result = new StringBuilder();
+            List<string> result = new List<string>();
             var ftp = (FtpWebRequest)WebRequest.Create(new Uri(Path.Combine("ftp://" + FtpServer, relativePath).Replace("\\", "/")));
             ftp.Credentials = new NetworkCredential(Username, Password);
             ftp.Method = WebRequestMethods.Ftp.ListDirectoryDetails;
@@ -437,16 +437,13 @@ namespace Masuit.Tools.Net
                     string line = reader.ReadLine();
                     while (line != null)
                     {
-                        result.Append(line);
-                        result.Append("\n");
+                        result.Add(line);
                         line = reader.ReadLine();
                     }
-
-                    result.Remove(result.ToString().LastIndexOf("\n", StringComparison.Ordinal), 1);
                 }
             }
 
-            return result.ToString().Split('\n');
+            return result;
         }
 
         /// <summary>
@@ -494,7 +491,7 @@ namespace Masuit.Tools.Net
         /// <returns></returns>
         public string[] GetDirectories(string relativePath)
         {
-            string[] drectory = GetFilesDetails(relativePath);
+            var drectory = GetFilesDetails(relativePath);
             string m = string.Empty;
             foreach (string str in drectory)
             {

+ 2 - 2
Masuit.Tools/Properties/AssemblyInfo.cs

@@ -36,7 +36,7 @@ using System.Runtime.InteropServices;
 // 方法是按如下所示使用“*”: :
 // [assembly: AssemblyVersion("1.0.*")]
 
-[assembly: AssemblyVersion("2.1.6.0")]
-[assembly: AssemblyFileVersion("2.1.6.0")]
+[assembly: AssemblyVersion("2.2.0.0")]
+[assembly: AssemblyFileVersion("2.2.0.0")]
 [assembly: NeutralResourcesLanguage("zh-CN")]
 

+ 11 - 40
Masuit.Tools/Systems/SnowFlake.cs

@@ -1,4 +1,5 @@
-using Masuit.Tools.Strings;
+using Masuit.Tools.DateTimeExt;
+using Masuit.Tools.Strings;
 using System;
 
 namespace Masuit.Tools.Systems
@@ -13,6 +14,7 @@ namespace Masuit.Tools.Systems
         private static long _machineId; //机器码
         private static long _datacenterId; //数据ID
         private static long _sequence; //计数从零开始
+        private static long _lastTimestamp = -1L; //最后时间戳
 
         private const long Twepoch = 687888001020L; //唯一时间随机量
 
@@ -25,8 +27,7 @@ namespace Masuit.Tools.Systems
         private const long MachineIdShift = SequenceBits; //机器码数据左移位数,就是后面计数器占用的位数
         private const long DatacenterIdShift = SequenceBits + MachineIdBits;
         private const long TimestampLeftShift = DatacenterIdShift + DatacenterIdBits; //时间戳左移动位数就是机器码+计数器总字节数+数据字节数
-        private const long SequenceMask = -1L ^ -1L << (int)SequenceBits; //一微秒内可以产生计数,如果达到该值则等到下一微秒在进行生成
-        private static long _lastTimestamp = -1L; //最后时间戳
+        private const long SequenceMask = -1L ^ -1L << (int)SequenceBits; //一毫秒内可以产生计数,如果达到该值则等到下一毫秒在进行生成
 
         private static readonly object SyncRoot = new object(); //加锁对象
         private static readonly NumberFormater NumberFormater = new NumberFormater(36);
@@ -48,7 +49,7 @@ namespace Masuit.Tools.Systems
         /// </summary>
         public SnowFlake()
         {
-            Snowflakes(0L, -1);
+            Snowflakes(0, -1);
         }
 
         /// <summary>
@@ -93,31 +94,6 @@ namespace Masuit.Tools.Systems
             }
         }
 
-        /// <summary>
-        /// 生成当前时间戳
-        /// </summary>
-        /// <returns>毫秒</returns>
-        private static long GetTimestamp()
-        {
-            return (long)(DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalMilliseconds;
-        }
-
-        /// <summary>
-        /// 获取下一微秒时间戳
-        /// </summary>
-        /// <param name="lastTimestamp"></param>
-        /// <returns></returns>
-        private static long GetNextTimestamp(long lastTimestamp)
-        {
-            long timestamp = GetTimestamp();
-            if (timestamp <= lastTimestamp)
-            {
-                timestamp = GetTimestamp();
-            }
-
-            return timestamp;
-        }
-
         /// <summary>
         /// 获取长整形的ID
         /// </summary>
@@ -126,28 +102,23 @@ namespace Masuit.Tools.Systems
         {
             lock (SyncRoot)
             {
-                long timestamp = GetTimestamp();
+                var timestamp = (long)DateTime.UtcNow.GetTotalMilliseconds();
                 if (_lastTimestamp == timestamp)
                 {
-                    //同一秒中生成ID
-                    _sequence = (_sequence + 1) & SequenceMask; //用&运算计算该秒内产生的计数是否已经到达上限
+                    //同一秒中生成ID
+                    _sequence = (_sequence + 1) & SequenceMask; //用&运算计算该秒内产生的计数是否已经到达上限
                     if (_sequence == 0)
                     {
-                        //一微秒内产生的ID计数已达上限,等待下一微
-                        timestamp = GetNextTimestamp(_lastTimestamp);
+                        //一毫秒内产生的ID计数已达上限,等待下一毫
+                        timestamp = (long)DateTime.UtcNow.GetTotalMilliseconds();
                     }
                 }
                 else
                 {
-                    //不同秒生成ID
+                    //不同秒生成ID
                     _sequence = 0L;
                 }
 
-                if (timestamp < _lastTimestamp)
-                {
-                    throw new Exception("时间戳比上一次生成ID时时间戳还小,故异常");
-                }
-
                 _lastTimestamp = timestamp; //把当前时间戳保存为最后生成ID的时间戳
                 long id = ((timestamp - Twepoch) << (int)TimestampLeftShift) | (_datacenterId << (int)DatacenterIdShift) | (_machineId << (int)MachineIdShift) | _sequence;
                 return id;

+ 1 - 1
Masuit.Tools/packages.config

@@ -3,6 +3,6 @@
   <package id="AngleSharp" version="0.9.11" targetFramework="net45" />
   <package id="HtmlSanitizer" version="4.0.199" targetFramework="net45" />
   <package id="Newtonsoft.Json" version="12.0.1" targetFramework="net45" />
-  <package id="SharpZipLib" version="1.0.0" targetFramework="net45" />
+  <package id="SharpZipLib" version="1.1.0" targetFramework="net45" />
   <package id="StackExchange.Redis" version="1.2.6" targetFramework="net45" />
 </packages>

+ 0 - 1
NetCoreTest/NetCoreTest.csproj

@@ -10,7 +10,6 @@
 
   <ItemGroup>
     <PackageReference Include="Microsoft.AspNetCore.App" />
-    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.1.2" PrivateAssets="All" />
   </ItemGroup>
 
   <ItemGroup>

+ 11 - 8
Test/Program.cs

@@ -1,6 +1,8 @@
-using Masuit.Tools.Strings;
+using Masuit.Tools;
 using Masuit.Tools.Systems;
 using System;
+using System.Collections.Generic;
+using System.Diagnostics;
 
 namespace Test
 {
@@ -10,13 +12,14 @@ namespace Test
         {
             var timer = HiPerfTimer.Execute(() =>
             {
-                //var dic = new Dictionary<string, int>();
-                //for (int i = 0; i < 1000000; i++)
-                //{
-                //    dic.Add(DateTime.Now.Ticks.ToBinary(36), 0);
-                //}
-                long s = new NumberFormater(16).FromString("1a");
-                Console.WriteLine(s);
+                var dic = new Dictionary<string, int>();
+                var sf = SnowFlake.GetInstance();
+                for (int i = 0; i < 1000000; i++)
+                {
+                    //Console.WriteLine(ObjectId.GenerateNewId());
+                    var id = Stopwatch.GetTimestamp().ToBinary(36);
+                    dic.Add(id, 0);
+                }
             });
             Console.WriteLine(timer);
         }

+ 0 - 38
Test/Test.csproj

@@ -36,43 +36,9 @@
     <CodeAnalysisRuleSet>..\..\..\Company\XiLife.NDC.WebAPI\XiLife.NDC.WebAPI\CodeRules\XiLife.ITC.Rules.CSharp.ruleset</CodeAnalysisRuleSet>
   </PropertyGroup>
   <ItemGroup>
-    <Reference Include="DnsClient, Version=1.2.0.0, Culture=neutral, PublicKeyToken=4574bb5573c51424, processorArchitecture=MSIL">
-      <HintPath>..\packages\DnsClient.1.2.0\lib\net45\DnsClient.dll</HintPath>
-    </Reference>
-    <Reference Include="MongoDB.Bson, Version=2.7.2.0, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\MongoDB.Bson.2.7.2\lib\net45\MongoDB.Bson.dll</HintPath>
-    </Reference>
-    <Reference Include="MongoDB.Driver, Version=2.7.2.0, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\MongoDB.Driver.2.7.2\lib\net45\MongoDB.Driver.dll</HintPath>
-    </Reference>
-    <Reference Include="MongoDB.Driver.Core, Version=2.7.2.0, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\MongoDB.Driver.Core.2.7.2\lib\net45\MongoDB.Driver.Core.dll</HintPath>
-    </Reference>
-    <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
-      <HintPath>..\packages\Newtonsoft.Json.12.0.1\lib\net45\Newtonsoft.Json.dll</HintPath>
-    </Reference>
-    <Reference Include="StackExchange.Redis, Version=1.2.6.0, Culture=neutral, processorArchitecture=MSIL">
-      <HintPath>..\packages\StackExchange.Redis.1.2.6\lib\net45\StackExchange.Redis.dll</HintPath>
-    </Reference>
     <Reference Include="System" />
-    <Reference Include="System.Buffers, Version=4.0.2.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
-      <HintPath>..\packages\System.Buffers.4.4.0\lib\netstandard1.1\System.Buffers.dll</HintPath>
-    </Reference>
     <Reference Include="System.Core" />
     <Reference Include="System.IO.Compression" />
-    <Reference Include="System.Management" />
-    <Reference Include="System.Runtime.InteropServices.RuntimeInformation, Version=4.0.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a, processorArchitecture=MSIL">
-      <HintPath>..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
-    </Reference>
-    <Reference Include="System.ValueTuple, Version=4.0.1.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51, processorArchitecture=MSIL">
-      <HintPath>..\packages\System.ValueTuple.4.3.0\lib\netstandard1.0\System.ValueTuple.dll</HintPath>
-    </Reference>
-    <Reference Include="System.Xml.Linq" />
-    <Reference Include="System.Data.DataSetExtensions" />
-    <Reference Include="Microsoft.CSharp" />
-    <Reference Include="System.Data" />
-    <Reference Include="System.Net.Http" />
-    <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
     <Compile Include="Program.cs" />
@@ -84,10 +50,6 @@
     <None Include="Test.ruleset" />
   </ItemGroup>
   <ItemGroup>
-    <ProjectReference Include="..\Masuit.Tools.NoSQL.MongoDBClient\Masuit.Tools.NoSQL.MongoDBClient.csproj">
-      <Project>{1d45bdc4-74c4-4356-8b93-7f7a09106bb8}</Project>
-      <Name>Masuit.Tools.NoSQL.MongoDBClient</Name>
-    </ProjectReference>
     <ProjectReference Include="..\Masuit.Tools\Masuit.Tools.csproj">
       <Project>{275d5a0d-c49c-497e-a4b5-f40285c2495f}</Project>
       <Name>Masuit.Tools</Name>

+ 0 - 1
Test/packages.config

@@ -8,5 +8,4 @@
   <package id="StackExchange.Redis" version="1.2.6" targetFramework="net45" />
   <package id="System.Buffers" version="4.4.0" targetFramework="net45" />
   <package id="System.Runtime.InteropServices.RuntimeInformation" version="4.3.0" targetFramework="net45" />
-  <package id="System.ValueTuple" version="4.3.0" targetFramework="net45" />
 </packages>