Entity Framework缓存异常:登录时返回旧用户数据,疑与DI相关
你遇到的核心问题是**EF一级缓存(DbContext级)**在作祟,再加上Unity DI的生命周期配置不当,尤其是DbContext和OAuth Provider的依赖注入方式有问题,导致修改用户数据后,登录时还是拿到旧数据。下面一步步给你解决:
1. 先搞定DbContext的生命周期(最关键)
EF的DbContext默认会缓存它查询到的所有实体,如果你把DbContext注册成了Singleton或者生命周期太长的模式,那它会一直复用旧数据。Web API里最合适的DbContext生命周期是每个请求一个实例,也就是用Unity的PerRequestLifetimeManager。
首先在你的Register方法里添加DbContext的注册(假设你的DbContext叫AppDbContext):
// 替换成你的实际DbContext类名 container.RegisterType<AppDbContext>(new PerRequestLifetimeManager());
同时,把所有依赖DbContext的Repo和服务也改成PerRequest,避免跨请求复用:
container.RegisterType<IRoleRepo, RoleRepo>(new PerRequestLifetimeManager()); container.RegisterType<IUserRepo, UserRepo>(new PerRequestLifetimeManager()); container.RegisterType<IMembershipService, MembershipService>(new PerRequestLifetimeManager()); container.RegisterType<IEncryptionService, EncryptionService>(new PerRequestLifetimeManager());
2. 修复OAuth Provider的依赖注入坑
你现在在ConfigureAuth里直接把IMembershipService注入到ApplicationOAuthProvider的构造函数里,但OAuthAuthorizationServerOptions.Provider是个单例(启动时创建后一直复用),这就导致MembershipService和它依赖的DbContext也变成了单例,永远用第一次创建的实例,自然一直返回旧数据。
解决方法是不在构造函数注入,而是在每次请求时从当前Owin上下文获取服务:
修改ApplicationOAuthProvider
public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider { private readonly string _publicClientId; // 只保留publicClientId参数,去掉IMembershipService注入 public ApplicationOAuthProvider(string publicClientId) { _publicClientId = publicClientId ?? throw new ArgumentNullException(nameof(publicClientId)); } public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context) { // 从当前请求的Owin上下文获取MembershipService var membershipService = context.Request.GetOwinContext().Get<IMembershipService>(); if (membershipService == null) { context.SetError("server_error", "无法加载用户服务"); return; } User user = membershipService.ValidateUser(context.UserName, context.Password); if (user == null) { context.SetError("invalid_grant", "Korisničko ime ili lozinka nisu ispravni."); return; } // 下面的身份凭证代码保持不变... ClaimsIdentity oAuthIdentity = new ClaimsIdentity(context.Options.AuthenticationType); foreach (var role in user.roles) { oAuthIdentity.AddClaim(new Claim(ClaimTypes.Role, role.role_name)); } oAuthIdentity.AddClaim(new Claim(ClaimTypes.NameIdentifier, user.username)); ClaimsIdentity cookiesIdentity = new ClaimsIdentity(context.Options.AuthenticationType); AuthenticationProperties properties = CreateProperties(context.UserName, Newtonsoft.Json.JsonConvert.SerializeObject(user.roles.Select(x => x.role_name)), user.Location.Naziv); AuthenticationTicket ticket = new AuthenticationTicket(oAuthIdentity, properties); ticket.Properties.AllowRefresh = true; ticket.Properties.ExpiresUtc = DateTimeOffset.Now.AddMinutes(30); context.Validated(ticket); context.Request.Context.Authentication.SignIn(cookiesIdentity); } // 其他方法不变... }
修改Startup里的ConfigureAuth
把Provider的实例化改成只传PublicClientId:
OAuthOptions = new OAuthAuthorizationServerOptions { TokenEndpointPath = new PathString("/api/users/token"), // 这里只传PublicClientId,不再注入IMembershipService Provider = new ApplicationOAuthProvider(PublicClientId), AuthorizeEndpointPath = new PathString("/api/users/login"), AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(30), AllowInsecureHttp = true };
还要确保Unity能在Owin上下文里解析服务,在Startup的Configuration方法里添加Unity中间件的配置:
public void Configuration(IAppBuilder app) { // 先添加这行,让Owin使用Unity解析服务 app.UseUnityMiddleware(IoC); // 然后再配置Auth ConfigureAuth(app); // 其他配置... }
3. 登录时强制获取最新数据(可选但保险)
如果上面的配置还不放心,可以在查询用户时强制绕过EF缓存,用AsNoTracking()让EF不缓存查询结果,每次都去数据库取最新数据:
修改EntityBaseRepo的GetSingle方法
添加一个参数控制是否禁用跟踪:
public T GetSingle(Expression<Func<T, bool>> predicate, bool asNoTracking = false, params Expression<Func<T, object>>[] includeProperties) { IQueryable<T> query = _context.Set<T>(); // 如果需要禁用跟踪,添加AsNoTracking if (asNoTracking) { query = query.AsNoTracking(); } foreach (var includeProperty in includeProperties) { query = query.Include(includeProperty); } return query.Where(predicate).SingleOrDefault(); }
在MembershipService的ValidateUser里调用时启用asNoTracking
public User ValidateUser(string username, string password) { // 第二个参数传true,禁用跟踪,强制取最新数据 var user = _userRepo.GetSingle(x => x.username == username, true, u => u.Location); // 密码验证逻辑... return user; }
这样一套操作下来,就能彻底解决登录时返回旧用户数据的问题了。
内容的提问来源于stack exchange,提问作者Milorad

