Skip to content

Releases: pdevito3/craftsman

v0.26.1

02 Feb 04:00
Compare
Choose a tag to compare

Updated

  • Remove unused swagger props

Fixed

  • Route builder includes version

Full Changelog: v0.26.0...v0.26.1

v0.26.0

02 Feb 03:47
Compare
Choose a tag to compare

Updated

  • Refactor to primary ctors
  • Removed deprecated (and partial) api versioning in favor of new versioning
    • Added versioning to controller routes
    • Updated versioning service
    • Updated swagger to support versioning

Full Changelog: v0.25.1...v0.26.0

v0.25.1

10 Jan 01:39
Compare
Choose a tag to compare

[0.25.1] - 01/09/2024

Updated

  • Better exception for smart enum errors
  • Sponsorship request
  • Bump naming conventions
  • Group Otel in dependabot

v0.25.0

29 Dec 03:32
Compare
Choose a tag to compare

Additions and Updates

  • Scaffolded projects use .NET 8
  • Bump Nuget packages
  • Bespoke DateTimeProvider removed in favor of the new built in TimeProvider
  • BaseEntity audit times use DateTimeOffset
  • Model classes use record type
  • Remove DateOnlyConverter for SqlServer since it's built into .NET 8
  • Moved PagedList to Resources directory
  • Underlying craftsman code uses .NET 8
  • Bump underlying .NET packages and remove unused packages in craftsman
  • Remove BFF commands
  • Remove BFF from examples

Fixed

  • Dependabot indent
  • Github test actions

v0.24.1

10 Nov 02:28
Compare
Choose a tag to compare

Fixed

  • Swagger config can handle nested DTO classes: config.CustomSchemaIds(type => type.ToString().Replace("+, "."));
  • Don't ignore default hangfire queue
  • Smart value object scaffolding doesn't use old enum logic for entity or fakes

Full Changelog: v0.24.0...v0.24.1

v0.24.0

24 Oct 03:13
Compare
Choose a tag to compare

Added

  • New IsLogMasked option for masking entity properties in logs
  • Dependabot scaffolding. Can be excluded with IncludeDependabot = false at the domain template level
  • Github test action scaffolding. Can be excluded with IncludeGithubTestActions = false at the api template level
  • Support for string[] when using Postgres
  • ValueObject 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
        
}}";

Updated

  • Logging was refactored to use app settings
  • Add missing HttpClientInstrumentation on OTel
  • SortOrder and Filters are nullable on list dto param
  • Remove old and unused fluent assertion options
  • Entity plural is more powerful with Humanizer

Fixed

  • Email setter

v0.23.2

30 Sep 03:31
Compare
Choose a tag to compare

Fix

  • Hangfire CompatibilityLevel updated to latest

v0.23.1

30 Sep 01:01
Compare
Choose a tag to compare

Fixed

  • Entity usings for data annotations (#126)
  • get all is plural (#127)

v0.23.0

23 Sep 01:39
Compare
Choose a tag to compare

Additions and Updates

  • New GetAll and Job 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 and Exceptions and ValueObject 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

  • Can handle no global git config (#122, #72)
  • Can use . in project name (#111)

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);
    }...
Read more

v0.22.1

13 Aug 18:47
Compare
Choose a tag to compare

Fixed

  • Can respect audience prop. fixes #119