我已经使用DDD构建了项目,看起来像这样:
| CompanyName.API
| MyControllerThatRequiresJwtToken << Entry point
| CompanyName.Application
| CompanyName.Data
| EfCoreContext << I would like to get the claims here
| CompanyName.Domain
| CompanyName.IoC
| CompanyName.Test
| CompanyName.UI
我正在使用 Z.EntityFramework.Plus.Audit.EFCore 审核所有数据更改。我将其添加到CompanyName.Data项目中,因为这是我的EF上下文所在的位置。
问题是:API中的所有请求都需要JWT令牌。我想在Audit对象中设置发送请求的人的用户名,该用户名将保存到数据库中,但是我无法访问我的数据层中的HttpContext。
获取此信息的最佳方法是什么?也许将IHttpContextAccessor注入数据层?使数据层“依赖于HTTP”听起来不是一个好计划。
更新
我不确定如何将其从Controller传递到上下文。我相信它需要以某种方式注入。
EfCoreContext.cs
的片段public override int SaveChanges()
{
var audit = new Audit();
audit.CreatedBy = "JWT Username"; // << I'd need it here
audit.PreSaveChanges(this);
var rowAffecteds = base.SaveChanges();
audit.PostSaveChanges();
if (audit.Configuration.AutoSavePreAction != null)
{
audit.Configuration.AutoSavePreAction(this, audit);
base.SaveChanges();
}
return rowAffecteds;
}
答案 0 :(得分:1)
我遇到了这种情况。我的解决方法是这样的:
1-在控制器上获取用户信息,并将该信息提供给您的dto(请求)对象。
我编写了用于获取用户ID的扩展程序:
public static string GetUserId(this HttpContext httpContext)
{
return httpContext?.User?.Claims?.FirstOrDefault(claim => claim.Type == ClaimTypes.NameIdentifier)?.Value ?? string.Empty;
}
请求对象:
public class CreateMenuRequest
{
public string MenuName { get; set; }
[JsonIgnore]
public string UpdatedBy { get; set; }
}
2-将用户信息设置为请求对象
控制器:
[HttpPost, Route("")]
public IActionResult CreateMenu([FromBody] CreateMenuRequest createMenuRequest)
{
if (createMenuRequest != null)
{
createMenuRequest.UpdatedBy = HttpContext.GetUserId();
}
CreateMenuResponse createMenuResponse = _menuService.CreateMenu(createMenuRequest);
return StatusCode(HttpStatusCode.Created.ToInt(), createMenuResponse);
}
3-在服务层中,经过验证和其他业务需求后,我将请求映射到实体对象。这样的实体对象:
public class Menu : IAudit, ISoftDeletable
{
public long Id { get; set; }
..........
public string UpdatedBy { get; set; }
public DateTime UpdateDate { get; set; }
public string CreatedBy { get; set; }
public DateTime CreateDate { get; set; }
public bool IsDeleted { get; set; }
}
4-覆盖SaveChanges以编辑UpdateDate和CreatedDate(如果添加了项,则通过在CreatedBy字段中设置的信息进行更新)。
public override int SaveChanges()
{
ChangeTracker.DetectChanges();
IEnumerable<EntityEntry> deletedEntities = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Deleted && t.Entity is ISof
foreach (EntityEntry deletedEntity in deletedEntities)
{
if (!(deletedEntity.Entity is ISoftDeletable item)) continue;
item.IsDeleted = true;
deletedEntity.State = EntityState.Modified;
}
IEnumerable<object> addedEntities = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Added && t.Entity is IAudit)
.Select(t => t.Entity);
IEnumerable<object> modifiedEntities = ChangeTracker.Entries()
.Where(t => t.State == EntityState.Modified && t.Entity is IAudit)
.Select(t => t.Entity);
DateTime now = DateTime.UtcNow;
Parallel.ForEach(addedEntities, o =>
{
if (!(o is IAudit item))
return;
item.CreateDate = now;
item.UpdateDate = now;
item.CreatedBy = item.UpdatedBy;
});
Parallel.ForEach(modifiedEntities, o =>
{
if (!(o is IAudit item))
return;
item.UpdateDate = now;
});
return base.SaveChanges();
}
答案 1 :(得分:0)
代替传递IHttpContextAccessor,在Controller中提取用户名,然后将其传递给需要它的方法。 我已经创建了这样的扩展方法,
public static class UserResolverService
{
public static Guid GetUserId(this ControllerBase controller)
{
var result = controller.User.FindFirstValue(ClaimTypes.NameIdentifier);
return Guid.Parse(result);
}
}
然后在您的服务方法调用中
entity= await DomainServices.CreateSomething(this.GetUserId(), dtoObject);
这样,您的服务就不直接依赖于HttpContext,因为它暴露了比所需更多的东西。但是,在任何地方传递用户名都会产生开销。
另一个选择是创建一个依赖于IHttpContext访问器的服务,并公开一个GetUserMethod。然后让其他服务依赖此服务来获取当前用户。这样,只有一个业务层服务将与IHttpContextAccessor耦合。最佳候选人将是您的UserProfile服务。
答案 2 :(得分:0)
创建所需数据的抽象,然后将其作为具有DI的服务注入DbContext
中。
// interface
public interface IAppPrincipal
{
string Name { get; }
}
// concrete class
public class AppPrincipal : IAppPrincipal
{
public AppPrincipal(string name)
{
Name = name;
}
public string Name { get; }
}
// db context
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options, IAppPrincipal principal = null)
{
Principal = principal;
}
public IAppPrincipal Principal { get; }
public override int SaveChanges()
{
var audit = new Audit();
audit.CreatedBy = Principal?.Name;
...
}
}
// service registration in API or MVC app
services.AddScoped<IAppPrincipal>(provider =>
{
var user = provider.GetService<IHttpContextAccessor>()?.HttpContext?.User;
return new AppPrincipal(user?.Identity?.Name);
});
答案 3 :(得分:0)
例如创建一个名为IApplicationUser的接口。为它提供所需的只读属性,例如ID,名称和其他内容。
创建一个实现
public class ApplicationUser : IApplicationUser
{
private readonly IHttpContextAccessor httpContextAccessor;
public ApplicationUser(IHttpContextAccessor httpContextAccessor)
{
this.httpConntextAccessor = httpContextAccessor;
}
public Guid Id => this.GetUserId();
public string Name => this.GetUserName();
private Guid GetUserId()
{
var subject = this.httpContextAccessor.HttpContext
.User.Identity.Claims
.FirstOrDefault(claim => claim.Type == JwtClaimTypes.Subject);
return Guid.TryParse(subject, out var id) ? id : Guid.Empty;
}
private Guid GetUserId()
{
return this.httpContextAccessor.HttpContext
.User.Identity.Claims
.FirstOrDefault(claim => claim.Type == JwtClaimTypes.PreferredUserName);
}
}
现在向您的DI容器注册。对于默认的MS IOC:
services.AddScoped<IApplicationUser, ApplicationUser>();
在需要的地方注入IApplicationUser并使用它来获取用户信息。
编辑:IHttpContextAccessor必须已注册。如果不是这种情况,也要这样做
services.AddScoped<IHttpContextAccessor, HttpContextAccessor>();
编辑2:只是为了澄清。这不是要在存储库中使用,或者您想调用它。重新考虑您的逻辑,以便您可以在保存数据之前将该信息传递给您的实体。