A .NET scaffolding tool to help you stop worrying about boilerplate and focus on your business logic 🚀
MIT License
Bot releases are visible (Hide)
Published by pdevito3 9 months ago
datetimeoffset
property typeFull Changelog: https://github.com/pdevito3/craftsman/compare/v0.26.1...v0.26.2
Published by pdevito3 9 months ago
Full Changelog: https://github.com/pdevito3/craftsman/compare/v0.26.0...v0.26.1
Published by pdevito3 9 months ago
Full Changelog: https://github.com/pdevito3/craftsman/compare/v0.25.1...v0.26.0
Published by pdevito3 10 months ago
Published by pdevito3 10 months ago
DateTimeProvider
removed in favor of the new built in TimeProvider
BaseEntity
audit times use DateTimeOffset
record
typeDateOnlyConverter
for SqlServer
since it's built into .NET 8PagedList
to Resources
directoryPublished by pdevito3 12 months ago
config.CustomSchemaIds(type => type.ToString().Replace("+, "."));
Full Changelog: https://github.com/pdevito3/craftsman/compare/v0.24.0...v0.24.1
Published by pdevito3 12 months ago
IsLogMasked
option for masking entity properties in logsIncludeDependabot = false
at the domain template levelIncludeGithubTestActions = false
at the api template levelstring[]
when using PostgresValueObject
property scaffolding⚠️ note there's a new new marker in db config:
public sealed class RecipeConfiguration : IEntityTypeConfiguration<Recipe>
{{
public void Configure(EntityTypeBuilder<Recipe> builder)
{{
// Relationship Marker -- Deleting or modifying this comment could cause incomplete relationship scaffolding
// Property Marker -- Deleting or modifying this comment could cause incomplete relationship scaffolding
}}";
SortOrder
and Filters
are nullable on list dto paramHumanizer
Published by pdevito3 about 1 year ago
Published by pdevito3 about 1 year ago
Published by pdevito3 about 1 year ago
GetAll
and Job
featuresfake
anymoreBasePaginationParameters
and Exceptions
and ValueObject
to api project (#124)Command
prop from feature scaffolding.
in project name (#111)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_170)
.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);
}
Published by pdevito3 about 1 year ago
Published by pdevito3 about 1 year ago
testcontainers
Sieve
in favor of QueryKit
CanFilter
and CanSort
properties. These can still be set in your code, but will no longer pollute your entities.MustNot
MigrationHostedService
will not conflict with dbcontext when adding entitiesFull Changelog: https://github.com/pdevito3/craftsman/compare/v0.21.1...v0.22.0
Published by pdevito3 over 1 year ago
Published by pdevito3 over 1 year ago
bool
option of UseCustomErrorHandler
on ApiTemplate
that defaults to a new error handler using Hellang.ProblemDetails
. If you don't want a dependency, you can use the existing custom one, but the hellang one is richer and I didn't want to reinvent the wheelI
on messagesPublished by pdevito3 over 1 year ago
IProjectService
renamed to IProjectScopedService
Published by pdevito3 over 1 year ago
Add
featurePublished by pdevito3 over 1 year ago
Published by pdevito3 over 1 year ago
Published by pdevito3 over 1 year ago