v0.23.0
Additions and Updates
- New
GetAll
andJob
features - Hangfire integration (details on adding to existing projects below)
- Moq -> NSubsititute
- Bump base page size limit to 500
- Added global usings to the test projects
- Entity variables in tests don't start with
fake
anymore - Moved
BasePaginationParameters
andExceptions
andValueObject
to api project (#124) - Package bumps and cleanup with the exception of pulumi
- Remove
Command
prop from feature scaffolding - Better property names for controllers and requests
Fixed
Adding Hangfire To an Existing Project
Install
<PackageReference Include="Hangfire" Version="1.8.5" />
<PackageReference Include="Hangfire.MemoryStorage" Version="1.8.0" />
Add this to your Infra Registration
services.SetupHangfire(env);
// ---
public static class HangfireConfig
{
public static void SetupHangfire(this IServiceCollection services, IWebHostEnvironment env)
{
services.AddScoped<IJobContextAccessor, JobContextAccessor>();
services.AddScoped<IJobWithUserContext, JobWithUserContext>();
// if you want tags with sql server
// var tagOptions = new TagsOptions() { TagsListStyle = TagsListStyle.Dropdown };
// var hangfireConfig = new MemoryStorageOptions() { };
services.AddHangfire(config =>
{
config
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseMemoryStorage()
.UseColouredConsoleLogProvider()
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
// if you want tags with sql server
// .UseTagsWithSql(tagOptions, hangfireConfig)
.UseActivator(new JobWithUserContextActivator(services.BuildServiceProvider()
.GetRequiredService<IServiceScopeFactory>()));
});
services.AddHangfireServer(options =>
{
options.WorkerCount = 10;
options.ServerName = $"PeakLims-{env.EnvironmentName}";
if (Consts.HangfireQueues.List().Length > 0)
{
options.Queues = Consts.HangfireQueues.List();
}
});
}
}
Update Program.cs
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
AsyncAuthorization = new[] { new HangfireAuthorizationFilter(scope.ServiceProvider) },
IgnoreAntiforgeryToken = true
});
Add queues to your consts
public static class HangfireQueues
{
// public const string MyFirstQueue = "my-first-queue";
public static string[] List()
{
return typeof(HangfireQueues)
.GetFields(BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy)
.Where(fi => fi.IsLiteral && !fi.IsInitOnly && fi.FieldType == typeof(string))
.Select(x => (string)x.GetRawConstantValue())
.ToArray();
}
}
Create the following files
namespace PeakLims.Resources.HangfireUtilities;
using Hangfire.Client;
using Hangfire.Common;
public class CurrentUserFilterAttribute : JobFilterAttribute, IClientFilter
{
public void OnCreating(CreatingContext context)
{
var argue = context.Job.Args.FirstOrDefault(x => x is IJobWithUserContext);
if (argue == null)
throw new Exception($"This job does not implement the {nameof(IJobWithUserContext)} interface");
var jobParameters = argue as IJobWithUserContext;
var user = jobParameters?.User;
if(user == null)
throw new Exception($"A User could not be established");
context.SetJobParameter("User", user);
}
public void OnCreated(CreatedContext context)
{
}
}
@@ -0,0 +1,23 @@
namespace PeakLims.Resources.HangfireUtilities;
using Hangfire.Dashboard;
public class HangfireAuthorizationFilter : IDashboardAsyncAuthorizationFilter
{
private readonly IServiceProvider _serviceProvider;
public HangfireAuthorizationFilter(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public Task<bool> AuthorizeAsync(DashboardContext context)
{
// TODO alt -- add login handling with cookie handling
// var heimGuard = _serviceProvider.GetService<IHeimGuardClient>();
// return await heimGuard.HasPermissionAsync(Permissions.HangfireAccess);
var env = _serviceProvider.GetService<IWebHostEnvironment>();
return Task.FromResult(env.IsDevelopment());
}
}
namespace PeakLims.Resources.HangfireUtilities;
using System.Security.Claims;
using Hangfire;
using Hangfire.Annotations;
using Hangfire.AspNetCore;
using Hangfire.Client;
using Hangfire.Common;
using Services;
public interface IJobWithUserContext
{
public string? User { get; set; }
}
public class JobWithUserContext : IJobWithUserContext
{
public string? User { get; set; }
}
public interface IJobContextAccessor
{
JobWithUserContext? UserContext { get; set; }
}
public class JobContextAccessor : IJobContextAccessor
{
public JobWithUserContext? UserContext { get; set; }
}
public class JobWithUserContextActivator : AspNetCoreJobActivator
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public JobWithUserContextActivator([NotNull] IServiceScopeFactory serviceScopeFactory) : base(serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory));
}
public override JobActivatorScope BeginScope(JobActivatorContext context)
{
var user = context.GetJobParameter<string>("User");
if (user == null)
{
return base.BeginScope(context);
}
var serviceScope = _serviceScopeFactory.CreateScope();
var userContextForJob = serviceScope.ServiceProvider.GetRequiredService<IJobContextAccessor>();
userContextForJob.UserContext = new JobWithUserContext {User = user};
return new ServiceJobActivatorScope(serviceScope);
}
}
namespace PeakLims.Resources.HangfireUtilities;
using Hangfire;
using Hangfire.Annotations;
public class ServiceJobActivatorScope : JobActivatorScope
{
private readonly IServiceScope _serviceScope;
public ServiceJobActivatorScope([NotNull] IServiceScope serviceScope)
{
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
}
public override object Resolve(Type type)
{
return ActivatorUtilities.GetServiceOrCreateInstance(_serviceScope.ServiceProvider, type);
}
public override void DisposeScope()
{
_serviceScope.Dispose();
}
}
Add a permission to Permissions
public const string HangfireAccess = nameof(HangfireAccess);
Update your CurrentUserService
public interface ICurrentUserService : IPeakLimsScopedService
{
@@ -17,26 +18,43 @@ public interface ICurrentUserService : IPeakLimsScopedService
public sealed class CurrentUserService : ICurrentUserService
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IJobContextAccessor _jobContextAccessor;
public CurrentUserService(IHttpContextAccessor httpContextAccessor, IJobContextAccessor jobContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
_jobContextAccessor = jobContextAccessor;
}
public ClaimsPrincipal? User => _httpContextAccessor.HttpContext?.User ?? CreatePrincipalFromJobContextUserId();
public string? UserId => User?.FindFirstValue(ClaimTypes.NameIdentifier);
public string? Email => User?.FindFirstValue(ClaimTypes.Email);
public string? FirstName => User?.FindFirstValue(ClaimTypes.GivenName);
public string? LastName => User?.FindFirstValue(ClaimTypes.Surname);
public string? Username => User
?.Claims
?.FirstOrDefault(x => x.Type is "preferred_username" or "username")
?.Value;
public string? ClientId => User
?.Claims
?.FirstOrDefault(x => x.Type is "client_id" or "clientId")
?.Value;
public bool IsMachine => ClientId != null;
private ClaimsPrincipal? CreatePrincipalFromJobContextUserId()
{
var userId = _jobContextAccessor?.UserContext?.User;
if (string.IsNullOrEmpty(userId))
{
return null;
}
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, userId)
};
var identity = new ClaimsIdentity(claims, $"hangfirejob-{userId}");
return new ClaimsPrincipal(identity);
}
}
Add this to your test fixture
services.ReplaceServiceWithSingletonMock<IBackgroundJobClient>();
Add this unit test to CurrentUserServiceTests
[Fact]
public void can_fallback_to_user_in_job_context()
{
// Arrange
var name = new Faker().Person.UserName;
var httpContextAccessor = Substitute.For<IHttpContextAccessor>();
httpContextAccessor.HttpContext.Returns((HttpContext)null);
var jobContextAccessor = new JobContextAccessor();
jobContextAccessor.UserContext = new JobWithUserContext()
{
User = name
};
var currentUserService = new CurrentUserService(httpContextAccessor, jobContextAccessor);
// Act & Assert
currentUserService.UserId.Should().Be(name);
}