fix: resolve nullability warnings, add CI/CD and security workflows, harden .gitignore

- Add nullable annotations across discovery interfaces, dynamic query
  models, and filter/aggregate types to eliminate CS8600-series warnings
- Replace unsafe cast in DynamicQueryHandlerBase with pattern match
- Add CI workflow (build --warnaserror + test on JP branch)
- Add weekly security vulnerability scan workflow
- Extend .gitignore with secret/credential patterns (.env, *.key, secrets/, credentials.json)

Co-Authored-By: Svrnty Inc. <eng@svrnty.com>
This commit is contained in:
Svrnty 2026-02-27 19:28:24 -05:00
parent 92231df745
commit 5f3602d071
17 changed files with 113 additions and 47 deletions

27
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [JP]
pull_request:
branches: [JP]
jobs:
build-and-test:
name: Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build (warnings as errors)
run: dotnet build --no-restore --warnaserror
- name: Test
run: dotnet test --no-build --verbosity normal

27
.github/workflows/security.yml vendored Normal file
View File

@ -0,0 +1,27 @@
name: Security
on:
push:
branches: [JP]
pull_request:
branches: [JP]
schedule:
- cron: "0 6 * * 1" # Weekly on Monday at 06:00 UTC
jobs:
vulnerability-scan:
name: .NET vulnerability scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Restore dependencies
run: dotnet restore
- name: Check for vulnerable packages
run: dotnet list package --vulnerable --include-transitive

11
.gitignore vendored
View File

@ -340,4 +340,13 @@ ASALocalRun/
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
healthchecksdb
# Secrets and credentials
.env
.env.local
.env.*
*.key
secrets/
.aws/
credentials.json

View File

@ -19,7 +19,7 @@ public sealed class CommandMeta : ICommandMeta
ServiceType = serviceType;
}
private CommandNameAttribute NameAttribute => CommandType.GetCustomAttribute<CommandNameAttribute>();
private CommandNameAttribute? NameAttribute => CommandType.GetCustomAttribute<CommandNameAttribute>();
public string Name
{
@ -32,7 +32,7 @@ public sealed class CommandMeta : ICommandMeta
public Type CommandType { get; }
public Type ServiceType { get; }
public Type CommandResultType { get; }
public Type? CommandResultType { get; }
public string LowerCamelCaseName
{

View File

@ -7,7 +7,7 @@ public interface ICommandMeta
string Name { get; }
Type CommandType { get; }
Type ServiceType { get; }
Type CommandResultType { get; }
Type? CommandResultType { get; }
string LowerCamelCaseName { get; }
}

View File

@ -5,8 +5,8 @@ namespace Svrnty.CQRS.Abstractions.Discovery;
public interface IQueryDiscovery
{
IQueryMeta FindQuery(string name);
IQueryMeta FindQuery(Type queryType);
IQueryMeta? FindQuery(string name);
IQueryMeta? FindQuery(Type queryType);
IEnumerable<IQueryMeta> GetQueries();
bool QueryExists(string name);
bool QueryExists(Type queryType);
@ -16,8 +16,8 @@ public interface ICommandDiscovery
{
bool CommandExists(string name);
bool CommandExists(Type commandType);
ICommandMeta FindCommand(string name);
ICommandMeta FindCommand(Type commandType);
ICommandMeta? FindCommand(string name);
ICommandMeta? FindCommand(Type commandType);
IEnumerable<ICommandMeta> GetCommands();
}

View File

@ -13,7 +13,7 @@ public class QueryMeta : IQueryMeta
QueryResultType = queryResultType;
}
protected virtual QueryNameAttribute NameAttribute => QueryType.GetCustomAttribute<QueryNameAttribute>();
protected virtual QueryNameAttribute? NameAttribute => QueryType.GetCustomAttribute<QueryNameAttribute>();
public virtual string Name
{

View File

@ -20,10 +20,10 @@ public interface IDynamicQuery<TSource, TDestination, out TParams> : IDynamicQue
public interface IDynamicQuery
{
List<IFilter> GetFilters();
List<IGroup> GetGroups();
List<ISort> GetSorts();
List<IAggregate> GetAggregates();
List<IFilter>? GetFilters();
List<IGroup>? GetGroups();
List<ISort>? GetSorts();
List<IAggregate>? GetAggregates();
int? GetPage();
int? GetPageSize();
}

View File

@ -3,5 +3,5 @@
public interface IDynamicQueryParams<out TParams>
where TParams : class
{
TParams GetParams();
TParams? GetParams();
}

View File

@ -22,7 +22,7 @@ public class DynamicQueryMeta(Type queryType, Type serviceType, Type queryResult
}
}
public Type ParamsType { get; internal set; }
public string OverridableName { get; internal set; }
public Type? ParamsType { get; internal set; }
public string? OverridableName { get; internal set; }
}

View File

@ -18,9 +18,9 @@ public class DynamicQuery<TSource, TDestination, TParams> : DynamicQuery, IDynam
where TDestination : class
where TParams : class
{
public TParams Params { get; set; }
public TParams? Params { get; set; }
public TParams GetParams()
public TParams? GetParams()
{
return Params;
}
@ -30,23 +30,23 @@ public class DynamicQuery : IDynamicQuery
{
public int? Page { get; set; }
public int? PageSize { get; set; }
public List<Sort> Sorts { get; set; }
public List<DynamicQueryAggregate> Aggregates { get; set; }
public List<Group> Groups { get; set; }
public List<DynamicQueryFilter> Filters { get; set; }
public List<Sort>? Sorts { get; set; }
public List<DynamicQueryAggregate>? Aggregates { get; set; }
public List<Group>? Groups { get; set; }
public List<DynamicQueryFilter>? Filters { get; set; }
public List<IAggregate> GetAggregates()
public List<IAggregate>? GetAggregates()
{
return Aggregates?.Select(t => t.ToAggregate())?.ToList();//.AsEnumerable<IAggregate>()?.ToList();
return Aggregates?.Select(t => t.ToAggregate())?.ToList();
}
public List<IFilter> GetFilters()
public List<IFilter>? GetFilters()
{
return Filters?.Select(t => t.ToFilter())?.ToList();
}
public List<IGroup> GetGroups()
public List<IGroup>? GetGroups()
{
return this.Groups?.AsEnumerable<IGroup>()?.ToList();
}
@ -61,7 +61,7 @@ public class DynamicQuery : IDynamicQuery
return this.PageSize;
}
public List<ISort> GetSorts()
public List<ISort>? GetSorts()
{
return this.Sorts?.AsEnumerable<ISort>()?.ToList();
}

View File

@ -6,8 +6,8 @@ namespace Svrnty.CQRS.DynamicQuery;
public class DynamicQueryAggregate
{
public string Path { get; set; }
public string Type { get; set; }
public required string Path { get; set; }
public required string Type { get; set; }
public IAggregate ToAggregate()
{

View File

@ -9,14 +9,14 @@ namespace Svrnty.CQRS.DynamicQuery;
public class DynamicQueryFilter
{
public List<DynamicQueryFilter> Filters { get; set; }
public List<DynamicQueryFilter>? Filters { get; set; }
public bool? And { get; set; }
public string Type { get; set; }
public string? Type { get; set; }
public bool? Not { get; set; }
public string Path { get; set; }
public object Value { get; set; }
public string? Path { get; set; }
public object? Value { get; set; }
public string QueryValue
public string? QueryValue
{
get
{
@ -32,7 +32,7 @@ public class DynamicQueryFilter
public IFilter ToFilter()
{
var type = Enum.Parse<FilterType>(Type);
var type = Enum.Parse<FilterType>(Type!);
if (type == FilterType.Composite)
{
var compositeFilter = new CompositeFilter
@ -44,7 +44,7 @@ public class DynamicQueryFilter
return compositeFilter;
}
object value = Value;
object? value = Value;
if (Value is JsonElement jsonElement)
{
switch (jsonElement.ValueKind)

View File

@ -60,7 +60,10 @@ public abstract class DynamicQueryHandlerBase<TSource, TDestination>
{
var types = _dynamicQueryInterceptorProviders.SelectMany(t => t.GetInterceptorsTypes()).Distinct();
foreach (var type in types)
yield return _serviceProvider.GetService(type) as IQueryInterceptor;
{
if (_serviceProvider.GetService(type) is IQueryInterceptor interceptor)
yield return interceptor;
}
}
protected async Task<IQueryExecutionResult<TDestination>> ProcessQueryAsync(IDynamicQuery query,

View File

@ -26,11 +26,11 @@ public static class ServiceCollectionExtensions
return new DynamicQueryServicesBuilder(services);
}
public static IServiceCollection AddDynamicQuery<TSourceAndDestination>(this IServiceCollection services, string name = null)
public static IServiceCollection AddDynamicQuery<TSourceAndDestination>(this IServiceCollection services, string? name = null)
where TSourceAndDestination : class
=> AddDynamicQuery<TSourceAndDestination, TSourceAndDestination>(services, name: name);
public static IServiceCollection AddDynamicQuery<TSource, TDestination>(this IServiceCollection services, string name = null)
public static IServiceCollection AddDynamicQuery<TSource, TDestination>(this IServiceCollection services, string? name = null)
where TSource : class
where TDestination : class
{
@ -51,7 +51,7 @@ public static class ServiceCollectionExtensions
return services;
}
public static IServiceCollection AddDynamicQueryWithProvider<TSource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TQueryableProvider>(this IServiceCollection services, string name = null)
public static IServiceCollection AddDynamicQueryWithProvider<TSource, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TQueryableProvider>(this IServiceCollection services, string? name = null)
where TQueryableProvider : class, IQueryableProvider<TSource>
where TSource : class
{
@ -60,7 +60,7 @@ public static class ServiceCollectionExtensions
return services;
}
public static IServiceCollection AddDynamicQueryWithParamsAndProvider<TSource, TParams, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TQueryableProvider>(this IServiceCollection services, string name = null)
public static IServiceCollection AddDynamicQueryWithParamsAndProvider<TSource, TParams, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] TQueryableProvider>(this IServiceCollection services, string? name = null)
where TQueryableProvider : class, IQueryableProvider<TSource>
where TParams : class
where TSource : class
@ -86,12 +86,12 @@ public static class ServiceCollectionExtensions
return services;
}
public static IServiceCollection AddDynamicQueryWithParams<TSourceAndDestination, TParams>(this IServiceCollection services, string name = null)
public static IServiceCollection AddDynamicQueryWithParams<TSourceAndDestination, TParams>(this IServiceCollection services, string? name = null)
where TSourceAndDestination : class
where TParams : class
=> AddDynamicQueryWithParams<TSourceAndDestination, TSourceAndDestination, TParams>(services, name: name);
public static IServiceCollection AddDynamicQueryWithParams<TSource, TDestination, TParams>(this IServiceCollection services, string name = null)
public static IServiceCollection AddDynamicQueryWithParams<TSource, TDestination, TParams>(this IServiceCollection services, string? name = null)
where TSource : class
where TDestination : class
where TParams : class

View File

@ -15,8 +15,8 @@ public sealed class CommandDiscovery : ICommandDiscovery
}
public IEnumerable<ICommandMeta> GetCommands() => _commandMetas;
public ICommandMeta FindCommand(string name) => _commandMetas.FirstOrDefault(t => t.Name == name);
public ICommandMeta FindCommand(Type commandType) => _commandMetas.FirstOrDefault(t => t.CommandType == commandType);
public ICommandMeta? FindCommand(string name) => _commandMetas.FirstOrDefault(t => t.Name == name);
public ICommandMeta? FindCommand(Type commandType) => _commandMetas.FirstOrDefault(t => t.CommandType == commandType);
public bool CommandExists(string name) => _commandMetas.Any(t => t.Name == name);
public bool CommandExists(Type commandType) => _commandMetas.Any(t => t.CommandType == commandType);
}

View File

@ -15,8 +15,8 @@ public sealed class QueryDiscovery : IQueryDiscovery
}
public IEnumerable<IQueryMeta> GetQueries() => _queryMetas;
public IQueryMeta FindQuery(string name) => _queryMetas.FirstOrDefault(t => t.Name == name);
public IQueryMeta FindQuery(Type queryType) => _queryMetas.FirstOrDefault(t => t.QueryType == queryType);
public IQueryMeta? FindQuery(string name) => _queryMetas.FirstOrDefault(t => t.Name == name);
public IQueryMeta? FindQuery(Type queryType) => _queryMetas.FirstOrDefault(t => t.QueryType == queryType);
public bool QueryExists(string name) => _queryMetas.Any(t => t.Name == name);
public bool QueryExists(Type queryType) => _queryMetas.Any(t => t.QueryType == queryType);
}