为什么我的 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 的返回用户。
故事的寓意是确认哪些对象在代码中的哪些点被使用,或者有一个对象被传入、更新,然后从函数返回以保持一致性。
我有一个 Azure SQL 数据库,最初有以下列:
用户名 密码散列 密码盐
此数据库提供 .NET Core C# API 检查用户名和密码以 return JWT 令牌。
API 有一个 User 对象,它由所有三列组成,类型正确,一个 DbContext 带有 DbSet
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 的返回用户。
故事的寓意是确认哪些对象在代码中的哪些点被使用,或者有一个对象被传入、更新,然后从函数返回以保持一致性。