为什么我的 API 更新后的 DbContext 不能获取新添加的数据库列?

Why won't my API's updated DbContext fetch a newly added DB column?

我有一个 Azure SQL 数据库,最初有以下列:

用户名 密码散列 密码盐

此数据库提供 .NET Core C# API 检查用户名和密码以 return JWT 令牌。

API 有一个 User 对象,它由所有三列组成,类型正确,一个 DbContext 带有 DbSet,一个 IServiceCollection 使用了所述 DbContext。

API 工作正常,return 根据需要获取 JWT 令牌。

从那以后我需要添加一个额外的参数来检查并传递给 JWT 创建 - 相关列已在数据库中创建,API 中的用户对象已更新以包含额外的参数,并且在整个 API 代码的智能感知中观察到该额外参数。

问题是当 API 部署到 Azure 时,额外的参数没有被识别和填充;如何使 API 正确更新以使用新的 DbContext 并使用额外参数检索用户?

(为简洁起见,我省略了接口,因为它们本质上是对应的 classes)

用户、UserRequest 和 MyApiDbContext 类:

using Microsoft.EntityFrameworkCore;

namespace MyApi.Models
{
    // Basic user model used for authentication
    public class User
    {
        public string UserId { get; set; }
        public byte[] PasswordHash { get; set; }
        public byte[] PasswordSalt { get; set; }
        public string ExtraParam { get; set; } // newly added parameter
    }

    public class UserRequest
    {
        public string UserId { get; set; }
        public string password { get; set; }
    }

    public class MyApiDbContext : DbContext
    {
        public MyApiDbContext(DbContextOptions<MyApiDbContext> options)
            : base(options)
        {
        }
                
        public DbSet<User> Users { get; set; }
    }
}

检索用户的 AuthRepository:

using Microsoft.EntityFrameworkCore;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;

namespace MyApi.Services
{
    public class AuthRepository : IAuthRepository
    {
        private readonly MyApiDbContext _context;
        public AuthRepository(MyApiDbContext context)
        {
            _context = context;
        }
        public async Task<User> Login(string username, string password)
        {
            // my test user gets returned
            User returnedUser = await _context.Users.FirstOrDefaultAsync(x => x.UserId == username);

            if (returnedUser == null)
            {
                return null;
            }

            // the password get verified
            if (!VerifyPasswordHash(password, returnedUser.PasswordHash, returnedUser.PasswordSalt))
            {
                return null;
            }

            // this does not get changed, but the value set in the DB is definitely a string
            if (returnedUser.ExtraParam == null || returnedUser.ExtraParam == "")
            {
                returnedUser.ExtraParam = "placeholder"
            }

            return returnedUser;
        }
    }
}

为用户调用 AuthRepository 然后“创建 JWT 令牌”的 AuthService(对于此示例只是 returning 字符串),当前设置为 return 用户详细信息:

using Microsoft.Extensions.Options;
using MyApi.Interfaces;
using MyApi.Models;
using System;
using System.Threading.Tasks;

namespace MyApi.Services
{
    public class AuthService : IAuthService
    {
        private readonly IOptions<MyApiBlobStorageOptions> _settings;
        private readonly IAuthRepository _repository;

        public AuthService(IOptions<MyApiBlobStorageOptions> settings, IAuthRepository repository)
        {
            _repository = repository;
            _settings = settings;
        }

        public async Task<string> Login(string username, string password)
        {
            User returnedUser = await _repository.Login(username, password);

            if (returnedUser != null)
            {
                // currently returns "UserIdInDB,ProvidedPasswordFromLogin,"
                return $"{returnedUser.UserId},{password},{returnedUser.ExtraParam}";
            }

            return null;
        }
    }
}

调用AuthService的控制器:

using Microsoft.AspNetCore.Mvc;
using MyApi.Interfaces;
using MyApi.Models;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

namespace MyApi.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class AuthController : ControllerBase
    {
        private readonly MyApiDbContext _context;
        private readonly IAuthService _authService;

        public AuthController(MyApiDbContext context, IAuthService authService)
        {
            _context = context;
            _authService = authService;
        }

        [HttpPost("login")]
        public async Task<IActionResult> Login(UserRequest loginUser)
        {
            string token = await _authService.Login(loginUser.UserId, loginUser.Password);

            if (token != null)
            {
                return Ok(token);
            }

            return Unauthorized("Access Denied!!");
        }
    }
}

注册一切的启动class:

using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using MyApi.Interfaces;
using MyApi.Models;
using MyApi.Services;
using Microsoft.Extensions.Azure;
using Azure.Storage.Queues;
using Azure.Storage.Blobs;
using Azure.Core.Extensions;
using System;

namespace MyApi
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        private readonly ILogger<Startup> _logger;
        private readonly IConfiguration _config;
        public Startup(ILogger<Startup> logger, IConfiguration config)
        {
            _logger = logger;
            _config = config;
        }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            // Add dBContext for DB
            services.AddDbContextPool<MyApiDbContext>(options => options.UseSqlServer(_config.GetConnectionString("MyAzureDb")));
            
            // Add DI Reference for Repository
            services.AddScoped<IAuthRepository, AuthRepository>();

            // Add DI Reference for Azure Blob Storage Processes
            services.AddScoped<IBlobService, AzureBlobService>();

            // DI Reference for AuthService
            services.AddScoped<IAuthService, AuthService>();

            // Add configuration section for Constructor Injection
            services.Configure<ApiBlobStorageOptions>(_config.GetSection("MyApiBlobStorage"));

            services.AddMvc(mvcOptions => mvcOptions.EnableEndpointRouting = false).SetCompatibilityVersion(CompatibilityVersion.Latest);

            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddJwtBearer(options =>
                {
                    options.TokenValidationParameters = new TokenValidationParameters
                    {
                        ValidateIssuerSigningKey = true,
                        IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII
                            .GetBytes(_config.GetSection("MyApiBlobStorage:Secret").Value)),
                        ValidateIssuer = false,
                        ValidateAudience = false
                    };
                    options.Events = new JwtBearerEvents()
                    {
                        OnAuthenticationFailed = context =>
                            {
                                _logger.LogWarning("Token authentication failed whilst attempting to upload file");

                                return Task.CompletedTask;
                            }
                    };
                });
            services.AddAzureClients(builder =>
            {
                builder.AddBlobServiceClient(Configuration["ConnectionStrings:MyApiBlobStorage/AzureBlobStorageConnectionString:blob"], preferMsi: true);
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            app.UseHttpsRedirection();

            app.UseCors(x => x.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader());
            app.UseAuthentication();
            app.UseMvc();
        }
    }
    internal static class StartupExtensions
    {
        public static IAzureClientBuilder<BlobServiceClient, BlobClientOptions> AddBlobServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
        {
            if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
            {
                return builder.AddBlobServiceClient(serviceUri);
            }
            else
            {
                return builder.AddBlobServiceClient(serviceUriOrConnectionString);
            }
        }
        public static IAzureClientBuilder<QueueServiceClient, QueueClientOptions> AddQueueServiceClient(this AzureClientFactoryBuilder builder, string serviceUriOrConnectionString, bool preferMsi)
        {
            if (preferMsi && Uri.TryCreate(serviceUriOrConnectionString, UriKind.Absolute, out Uri serviceUri))
            {
                return builder.AddQueueServiceClient(serviceUri);
            }
            else
            {
                return builder.AddQueueServiceClient(serviceUriOrConnectionString);
            }
        }
    }
}

让我知道是否还有其他需要理解的东西:之前和现在之间的唯一区别是为 API 添加了 ExtraParam 和相应的引用,并且数据库获得了同名列.

我尝试添加参数并将其部署到 Azure 并正常发出 POST 请求,启动和停止应用程序服务,在应用程序服务停止时部署 API 并启动它再次,并重新启动应用程序服务。我不知道我可以在多大程度上尝试改变我正在做的事情,我正在尝试做与以前完全相同的事情,但需要从数据库请求一个额外的参数。

我还可以确认数据库包含 ExtraParam 列,并且它包含针对现有数据行的值,如使用 Azure 门户的数据库查询编辑器所查看的那样。

我已经解决了这个问题,部分原因是发布了这个问题并清理了 public 讨论的代码。

在登录控制器中,在我的开发代码中,随后忽略了返回用户的请求,传递了具有空 ExtraParam 的用户请求详细信息,而不是填充了 ExtraParam 的返回用户。

故事的寓意是确认哪些对象在代码中的哪些点被使用,或者有一个对象被传入、更新,然后从函数返回以保持一致性。