65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 .NET Core 3.1 通过静态 URL 提供数据库中存储的图像

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (8投票s)

2020 年 9 月 11 日

CPOL

6分钟阅读

viewsIcon

29725

downloadIcon

321

一个小的 API 应用程序,使用 .NET Core 3.1 编写,用于演示如何使用普通的静态图像 URL 从数据库检索图像

引言

今天,我们将创建一个简单的 API,它有一个用于上传图像的单一终结点。为此,我们将使用 Visual Studio 2019 中的 ASP.NET Core Web 应用程序 / API 模板。为了简化测试,我们还将向解决方案添加 Swagger 和 Entity Framework。结果将是,我们可以通过这样的终结点上传图像

解决方案

要阅读本文,您需要在本地环境中安装 Visual Studio 2019 和 .NET Core 3.1。

创建解决方案

第一步是在 Visual Studio 中创建一个新项目,然后选择“ASP.NET Core Web 应用程序”模板。给项目起一个新的名字,我选择了“Get-images-from-db-by-url”,然后在下一个屏幕上,选择“API”,如下图所示

此模板为您提供了一个简单的 API,它返回一些代表天气预报的 JSON 数据。现在我们将把这个基本解决方案变成更有趣的东西。

首先,我们更改 `Get-images-from-db-by-url` 项目的属性设置,以便在调试时,您将看到应用程序的根目录“/”而不是“/weatherforecast” URL。您可以在此处找到此设置

在“启动浏览器”复选框右侧的字段中,将 `weatherforecast` 替换为空字符串。这将使 Visual Studio 使用“/” URL 而不是“/weatherforecast” URL 来启动应用程序。

在我们可以开始做有趣的事情之前,我们还要做另外两件事

  1. 添加 Swagger 以简化将图像上传到后端的流程。
  2. 添加 Entity Framework 以支持从数据库写入/读取数据。

添加 Swagger

Swagger 是一个为您的 API 添加自动生成文档和 UI 的工具。通过在程序包管理器控制台中运行以下命令,将 Swagger 安装到您的项目中。

Install-Package Swashbuckle.AspNetCore

之后,您需要对 `Startup.cs` 文件进行两处更改。您的 `Startup.cs` 文件中应该已经有一个用于“ConfigureServices”的方法。将 `services.AddSwaggerGen()` 添加到其中。结果应该如下所示

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddSwaggerGen();
}

在名为“Configure”的方法中,您应该添加以下两行

app.UseSwagger();
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    c.RoutePrefix = string.Empty;
});

现在您可以尝试调试应用程序,希望您能看到 Swagger UI 界面。

添加 Entity Framework

通过在程序包管理器控制台中运行以下两个命令将 Entity Framework 添加到您的项目中

Install-Package Microsoft.EntityFrameworkCore.Sqlite
Install-Package Microsoft.EntityFrameworkCore.Tools

然后创建一个名为“Models”的新文件夹,并在该文件夹中创建一个名为 `Image` 的新类。在本例中,`image` 类将代表单个图像。在这里,我们需要 `Id` 属性、一个表示二进制图像数据的属性以及一个存储文件后缀的属性。

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace Get_images_from_db_by_url.Models
{
    public class Image
    {
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        [Key]
        public Guid Id { get; set; }
        public byte[] Data { get; set; }
        public string Suffix { get; set; }
    }
}

在完成 Entity Framework 的设置之前,我们还需要创建一个 `DBContext` 类。将以下类添加到项目中

using Microsoft.EntityFrameworkCore;
using Get_images_from_db_by_url.Models;

namespace Get_images_from_db_by_url
{
    public class EFContext : DbContext
    {
        public DbSet<img /> Images { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options) => 
                                       options.UseSqlite("Data Source=database.db");
    }
}

现在运行以下两个命令来创建初始迁移和解决方案中的 `database.db` 文件

Add-Migration InitialCreate
Update-Database

实现上传图像的 API 终结点

希望您的解决方案现在应该可以正常构建。在继续之前,我们应该清理一下现有的解决方案。我们首先将 `Controllers/WeatherForecastController.cs` 文件重命名为 `Controllers/ImageController.cs`,同时更新类名以匹配新文件名。然后删除 `WeatherForecast.cs` 文件。

将 `ImageController` 的内容替换为以下内容

using System;
using System.IO;
using System.Threading.Tasks;
using Get_images_from_db_by_url.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Get_images_from_db_by_url.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ImageController : ControllerBase
    {

        [HttpPost("image")]
        public async Task<guid> Image(IFormFile image)
        {
            using (var db = new EFContext())
            {
                using (var ms = new MemoryStream())
                {
                    image.CopyTo(ms);
                    var img = new Image()
                    {
                        Suffix = Path.GetExtension(image.FileName),
                        Data = ms.ToArray()
                    };

                    db.Images.Add(img);
                    await db.SaveChangesAsync();
                    return img.Id;
                }
            }
        }
    }
}

此“Image”方法将 `IFormFile` 作为参数,使用内存流对象读取它,并将结果存储在数据库中,然后将 `Id`(一个生成的 GUID)返回给用户。

在单击“尝试一下”按钮后,您的 Swagger 界面应该看起来像这样

上传图像后,您应该会收到一个 GUID 返回

实现从数据库检索图像的中间件

在 .NET 4.X 中,您将使用 HTTP Handler 来实现此功能,但在 .NET Core 中,这种 Handler 的概念已经消失了。我们想要的是拦截请求,分析它,如果请求匹配某些预定义的条件,我们就想从数据库中向用户提供一个图像。

我们要处理的条件是 URL 路径是否包含“/dynamic/images/”,以及所请求的资源是否具有 png、jpg 或 gif 扩展名。

在项目中创建一个新文件夹,并将其命名为“Middleware”。在这个新文件夹中,创建两个新类:`DynamicImageMiddleware` 和 `DynamicImageProvider`。

`DynamicImageProvider` 的内容应该如下

using Microsoft.AspNetCore.Http;
using Microsoft.EntityFrameworkCore;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;

namespace Get_images_from_db_by_url.Middleware
{
    public class DynamicImageProvider
    {
        private readonly RequestDelegate next;
        public IServiceProvider service;
        private string pathCondition = "/dynamic/images/";

        public DynamicImageProvider(RequestDelegate next)
        {
            this.next = next;
        }

        private static readonly string[] suffixes = new string[] {
            ".png",
            ".jpg",
            ".gif"
        };

        private bool IsImagePath(PathString path)
        {
            if (path == null || !path.HasValue)
                return false;

            return suffixes.Any
                   (x => path.Value.EndsWith(x, StringComparison.OrdinalIgnoreCase));
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                var path = context.Request.Path;
                if (IsImagePath(path) && path.Value.Contains(pathCondition))
                {
                    byte[] buffer = null;
                    var id = Guid.Parse(Path.GetFileName(path).Split(".")[0]);
                    
                    using (var db = new EFContext())
                    {
                        var imageFromDb = 
                           await db.Images.SingleOrDefaultAsync(x => x.Id == id);
                        buffer = imageFromDb.Data;
                    }

                    if (buffer != null && buffer.Length > 1)
                    {
                        context.Response.ContentLength = buffer.Length;
                        context.Response.ContentType = "image/" + 
                                            Path.GetExtension(path).Replace(".","");
                        await context.Response.Body.WriteAsync(buffer, 0, buffer.Length);
                    }
                    else
                        context.Response.StatusCode = 404;
                }
            }
            finally
            {
                if (!context.Response.HasStarted)
                    await next(context);
            }
        }
    }
}

这里没有什么复杂的东西。`.NET Core` 框架将执行“Invoke(HttpContext context)”方法,通过 `HttpContext`,您可以访问 `request` 和 `response` 对象。

我们只对 URL 路径包含“/dynamic/images”且扩展名为 png、jpg 或 gif 的请求感兴趣。

我们从路径中检索图像 GUID,从数据库中检索图像,并设置响应内容。由于响应对象需要一个流,因此在这种情况下,我们需要使用内存流。

`finally` 部分的内容非常重要,因为您不希望在向响应写入内容后将其发送到链中的任何其他中间件。但是,如果不是这样,您希望它通过 `next(context)` 方法继续正常路由。

下一步是添加 `DymaicImageMiddleware.cs` 文件的内容

using Microsoft.AspNetCore.Builder;

namespace Get_images_from_db_by_url.Middleware
{
    public static class DynamicImageMiddleware
    {
        public static IApplicationBuilder UseDynamicImageMiddleware
                                          (this IApplicationBuilder builder)
        {
            return builder.UseMiddleware<dynamicimageprovider>();
        }
    }
}

此文件包含一个扩展方法,将在 `Startup.cs` 文件中使用。

在 `Startup.cs` 文件的“Configure”方法中使用此扩展方法,在我这里是在“app.UseRouting()”行之后。

app.UseDynamicImageMiddleware();

如何测试

您可以通过以下步骤进行测试

  1. 使用 Swagger UI 和 Image POST 终结点上传图像
  2. 记下上传文件的扩展名和 API 返回的 GUID
  3. 执行 `/dynamic/images/[GUID].[EXTENSION]`,例如在我的情况下:`https://:44362/dynamic/images/36400564-b28d-45a3-a259-0afbb8c5ec51.png`,希望您能看到似乎是来自静态 URL 的静态图像,但实际上,该图像是从数据库中的表中提供的。

可能的改进

本文介绍的这种方法的一个优点是,通过静态 URL 提供的图像可以在浏览器中进行缓存。您可以通过向 `response` 对象添加“Expires”标头来控制缓存,如下所示

string ExpireDate = DateTime.UtcNow.AddDays(10)
                            .ToString("ddd, dd MMM yyyy HH:mm:ss", 
                                      System.Globalization.CultureInfo.InvariantCulture);
context.Response.Headers.Add("Expires", ExpireDate + " GMT");

此外,如果您觉得需要,还可以利用服务器端内存缓存。一个常见的用例是,通过在查询字符串中提供图像的长度和宽度来支持服务器端图像缩放。

历史

  • 2020 年 9 月 8 日:初始版本
© . All rights reserved.