craftsman

A .NET scaffolding tool to help you stop worrying about boilerplate and focus on your business logic 🚀

MIT License

Stars
1.1K

Bot releases are visible (Hide)

craftsman - v0.26.3 Latest Release

Published by pdevito3 7 months ago

Updated

  • Use newer header append method on paginated list endpoint

Fixed

  • Indentation for smart enumvalue objects
craftsman - v0.26.2

Published by pdevito3 9 months ago

Fixed

  • Fix typo on smart enum error
  • Can recognize datetimeoffset property type
  • Versioning in functional tests

Full Changelog: https://github.com/pdevito3/craftsman/compare/v0.26.1...v0.26.2

craftsman - v0.26.1

Published by pdevito3 9 months ago

Updated

  • Remove unused swagger props

Fixed

  • Route builder includes version

Full Changelog: https://github.com/pdevito3/craftsman/compare/v0.26.0...v0.26.1

craftsman - v0.26.0

Published by pdevito3 9 months ago

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: https://github.com/pdevito3/craftsman/compare/v0.25.1...v0.26.0

craftsman - v0.25.1

Published by pdevito3 9 months ago

[0.25.1] - 01/09/2024

Updated

  • Better exception for smart enum errors
  • Sponsorship request
  • Bump naming conventions
  • Group Otel in dependabot
craftsman - v0.25.0

Published by pdevito3 10 months ago

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
craftsman - v0.24.1

Published by pdevito3 11 months ago

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: https://github.com/pdevito3/craftsman/compare/v0.24.0...v0.24.1

craftsman - v0.24.0

Published by pdevito3 12 months ago

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
craftsman - v0.23.2

Published by pdevito3 about 1 year ago

Fix

  • Hangfire CompatibilityLevel updated to latest
craftsman - v0.23.1

Published by pdevito3 about 1 year ago

Fixed

  • Entity usings for data annotations (#126)
  • get all is plural (#127)
craftsman - v0.23.0

Published by pdevito3 about 1 year ago

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_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);
    }
craftsman - v0.22.1

Published by pdevito3 about 1 year ago

Fixed

  • Can respect audience prop. fixes #119
craftsman - v0.22.0

Published by pdevito3 about 1 year ago

What's Changed

  • Revamped Relationships to a new model
  • Integration and Functional tests use the latest versions and implementations of testcontainers
  • Removed Sieve in favor of QueryKit
  • Removed CanFilter and CanSort properties. These can still be set in your code, but will no longer pollute your entities.
  • Use top level routing for health checks and controllers
  • Update OTel to latest with newer syntax
  • Remove other base specification things from generic repo
  • Commands/Queries use records for input
  • DTOs are records
  • Additional validation exception extension method for MustNot

Fixed

  • Sql server setups can handle dateonly
  • Duplicate usings
  • Remove bad serilog enricher and update logger for env name
  • MigrationHostedService will not conflict with dbcontext when adding entities
  • fix: get endpoint when name == plural
  • Bugfix, Change AddProducerCommand to AddMessageCommand by @Ken1Nil in https://github.com/pdevito3/craftsman/pull/118

New Contributors

Full Changelog: https://github.com/pdevito3/craftsman/compare/v0.21.1...v0.22.0

craftsman - v0.21.1

Published by pdevito3 over 1 year ago

Fixed

  • Remove lingering mapper from empty feature
  • Fix connection string prop in dev app settings
  • Fix using statement for interop
craftsman - v0.21.0

Published by pdevito3 over 1 year ago

Added

  • New 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 wheel

Updated

  • Major MediatR update to 12.x
  • Features returning bool will now have no return value
  • Mapster -> Mapperly
  • Missing test projects won't cause failure

Fixed

  • Can better handle unneccessary I on messages
  • Mapper scaffolding
craftsman - v0.20.0

Published by pdevito3 over 1 year ago

Added

  • Specification support in repositories
  • Support for array property types

Updates

  • No more manipulation DTO
  • No default new guid on DTO
  • Tests use builder methods
  • No more autofaker for domain entity, only a builder
  • Entity properties not virtualized anymore
  • IProjectService renamed to IProjectScopedService
craftsman - v0.19.4

Published by pdevito3 over 1 year ago

Updates

  • Removed extra db call on Add feature
craftsman - v0.19.3

Published by pdevito3 over 1 year ago

Fixed

  • Fix bracket on DTOs
craftsman - v0.19.2

Published by pdevito3 over 1 year ago

Fixed

  • DTO indentations
craftsman - v0.19.1

Published by pdevito3 over 1 year ago

Updates

  • Removed custom dateonly and timeonly json converters in facor of built in .net 7 options