This CleanCodeJN package streamlines the development of web APIs in .NET applications by providing a robust framework for CRUD operations and facilitating the implementation of complex business logic in a clean and maintainable manner.
$ dotnet add package CleanCodeJN.GenericApisBuild production-ready APIs instantly – from Minimal APIs and Controllers to fully integrated GraphQL endpoints with CRUD, filtering, sorting & paging – powered by Mediator, AutoMapper, EF Core, FluentValidation, and the IOSP architecture pattern.
Integration Operation Segregation Principle
Split your request handlers into: Operations → contain real logic (DB, API, etc.)
Integrations → just orchestrate other request handlers
DataRepositoriesbuilder.Services.AddCleanCodeJN<MyDbContext>(options => {});/// <summary>
/// The options for the CleanCodeJN.GenericApis
/// </summary>
public class CleanCodeOptions
{
/// <summary>
/// The assemblies that contain the command types, Entity types and DTO types for automatic registration of commands, DTOs and entities.
/// </summary>
public List<Assembly> ApplicationAssemblies { get; set; } = [];
/// <summary>
/// The assembly that contains the validators types for using Fluent Validation.
/// </summary>
public Assembly ValidatorAssembly { get; set; }
/// <summary>
/// The assembly that contains the automapper mapping profiles.
/// </summary>
public Action<IMapperConfigurationExpression> MappingOverrides { get; set; }
/// <summary>
/// If true: Use distributed memory cache. If false: you can add another Distributed Cache implementation.
/// </summary>
public bool UseDistributedMemoryCache { get; set; } = true;
/// <summary>
/// If true: Add default logging behavior. If false: you can add another logging behavior.
/// </summary>
public bool AddDefaultLoggingBehavior { get; set; }
/// <summary>
/// Mediatr Types of Open Behaviors to register
/// </summary>
public List<Type> OpenBehaviors { get; set; } = [];
/// <summary>
/// Mediatr Types of Closed Behaviors to register
/// </summary>
public List<Type> ClosedBehaviors { get; set; } = [];
/// <summary>
/// Gets or sets a value indicating whether GraphQL auto-wiring is enabled.
/// </summary>
public GraphQLOptions GraphQLOptions { get; set; }
}app.UseCleanCodeJNWithMinimalApis();app.UseCleanCodeJNWithGraphQL();builder.Services.AddControllers()
.AddNewtonsoftJson(); // this is needed for "http patch" only. If you do not need to use patch, you can remove this line
// After Build()
app.MapControllers();builder.Services.AddCleanCodeJN<MyDbContext>(options =>
{
options.ApplicationAssemblies =
[
typeof(CleanCodeJN.GenericApis.Sample.Business.AssemblyRegistration).Assembly,
typeof(CleanCodeJN.GenericApis.Sample.Core.AssemblyRegistration).Assembly,
typeof(CleanCodeJN.GenericApis.Sample.Domain.AssemblyRegistration).Assembly
];
options.ValidatorAssembly = typeof(CleanCodeJN.GenericApis.Sample.Core.AssemblyRegistration).Assembly;
// Enable GraphQL with all CRUD operations
options.GraphQLOptions = new GraphQLOptions
{
Get = true,
Create = true,
Update = true,
Delete = true,
AddAuthorizationWithPolicyName = "MyPolicy", // optional for adding authorization policy
};
});
// Optional: Add Authentication and Authorization if needed
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("MyPolicy", policy =>
{
policy.RequireAuthenticatedUser();
policy.RequireClaim("role", "admin");
});
});public class CustomersV1Api : IApi
{
public List<string> Tags => ["Customers Minimal API"];
public string Route => $"api/v1/Customers";
public List<Func<WebApplication, RouteHandlerBuilder>> HttpMethods =>
[
app => app.MapGet<Customer, CustomerGetDto, int>(
Route,
Tags,
where: x => x.Name.StartsWith("Customer"),
includes: [x => x.Invoices],
select: x => new Customer { Name = x.Name },
ignoreQueryFilters: true),
app => app.MapGetPaged<Customer, CustomerGetDto, int>(Route, Tags),
app => app.MapGetFiltered<Customer, CustomerGetDto, int>(Route, Tags),
app => app.MapGetById<Customer, CustomerGetDto, int>(Route, Tags),
app => app.MapPut<Customer, CustomerPutDto, CustomerGetDto>(Route, Tags),
app => app.MapPost<Customer, CustomerPostDto, CustomerGetDto>(Route, Tags),
app => app.MapPatch<Customer, CustomerGetDto, int>(Route, Tags),
// Or use a custom Command with MapDeleteRequest()
app => app.MapDeleteRequest<Customer, CustomerGetDto, int>(Route, Tags, id => new DeleteCustomerIntegrationRequest { Id = id })
];
}public class CustomersV1Api : IApi
{
public List<string> Tags => ["Customers Minimal API"];
public string Route => $"api/v1/Customers";
public List<Func<WebApplication, RouteHandlerBuilder>> HttpMethods =>
[
app => app.MapGet<Customer, CustomerGetDto, int>(Route, Tags, where: x => x.Name.StartsWith("a"), select: x => new Customer { Name = x.Name }),
];
}[Tags("Customers Controller based")]
[Route($"api/v2/[controller]")]
public class CustomersController(IMediator commandBus, IMapper mapper)
: ApiCrudControllerBase<Customer, CustomerGetDto, CustomerPostDto, CustomerPutDto, int>(commandBus, mapper)
{
}/// <summary>
/// Customers Controller based
/// </summary>
/// <param name="commandBus">IMediatr instance.</param>
/// <param name="mapper">Automapper instance.</param>
[Tags("Customers Controller based")]
[Route($"api/v2/[controller]")]
public class CustomersController(IMediator commandBus, IMapper mapper)
: ApiCrudControllerBase<Customer, CustomerGetDto, CustomerPostDto, CustomerPutDto, int>(commandBus, mapper)
{
/// <summary>
/// Where clause for the Get method.
/// </summary>
public override Expression<Func<Customer, bool>> GetWhere => x => x.Name.StartsWith("Customer");
/// <summary>
/// Includes for the Get method.
/// </summary>
public override List<Expression<Func<Customer, object>>> GetIncludes => [x => x.Invoices];
/// <summary>
/// Select for the Get method.
/// </summary>
public override Expression<Func<Customer, Customer>> GetSelect => x => new Customer { Id = x.Id, Name = x.Name };
/// <summary>
/// AsNoTracking for the Get method.
/// </summary>
public override bool AsNoTracking => true;
}{
"Condition" : 0, // 0 = AND; 1 = OR
"Filters": [
{
"Field": "Name",
"Value": "aac",
"Type": 0
},
{
"Field": "Id",
"Value": "3",
"Type": 1
}
]
}Which means: Give me all Names which CONTAINS "aac" AND have Id EQUALS 3. So string Types use always CONTAINS and integer types use EQUALS. All filters are combined with ANDs.
public enum FilterTypeEnum
{
STRING = 0,
INTEGER = 1,
DOUBLE = 2,
INTEGER_NULLABLE = 3,
DOUBLE_NULLABLE = 4,
DATETIME = 5,
DATETIME_NULLABLE = 6,
GUID = 7,
GUID_NULLABLE = 8,
}Just write your AbstractValidators. They will be automatically executed on generic POST and generic PUT actions:
public class CustomerPostDtoValidator : AbstractValidator<CustomerPostDto>
{
public CustomerPostDtoValidator()
{
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(10);
}public class CustomerPutDtoValidator : AbstractValidator<CustomerPutDto>
{
public CustomerPutDtoValidator()
{
RuleFor(x => x.Id)
.GreaterThan(0);
RuleFor(x => x.Name)
.NotEmpty()
.MaximumLength(10)
.CreditCard();
}
}public class SpecificDeleteRequest : IRequest<BaseResponse<Customer>>
{
public required int Id { get; init; }
}public class SpecificDeleteRequest : IRequest<BaseResponse<Customer>>, ICachableRequest
{
public required int Id { get; init; }
public bool BypassCache { get; }
public string CacheKey => "Your Key";
public TimeSpan? CacheDuration => TimeSpan.FromHours(168);
}public class SpecificDeleteCommand(IRepository<Customer, int> repository) : IRequestHandler<SpecificDeleteRequest, BaseResponse<Customer>>
{
public async Task<BaseResponse<Customer>> Handle(SpecificDeleteRequest request, CancellationToken cancellationToken)
{
var deletedCustomer = await repository.Delete(request.Id, cancellationToken);
return await BaseResponse<Customer>.Create(deletedCustomer is not null, deletedCustomer);
}
}public class YourIntegrationCommand(ICommandExecutionContext executionContext)
: IntegrationCommand<YourIntegrationRequest, YourDomainObject>(executionContext)public static ICommandExecutionContext CustomerGetByIdRequest(
this ICommandExecutionContext executionContext, int customerId)
=> executionContext.WithRequest(
() => new GetByIdRequest<Customer>
{
Id = customerId,
Includes = [x => x.Invoices, x => x.OtherDependentTable],
},
CommandConstants.CustomerGetById); executionContext.WithParallelWhenAllRequests(
[
() => new GetByIdRequest<Customer, int>
{
Id = request.Id,
},
() => new GetByIdRequest<Customer, int>
{
Id = request.Id,
},
]) .WithRequest(
() => new YourSpecificRequest
{
Results = executionContext.GetListParallelWhenAll("Parallel Block"),
}) .WithRequest(
() => new GetByIdRequest<Invoice, Guid>
{
Id = executionContext.GetParallelWhenAllByIndex<Invoice>("Parallel Block", 1).Id,
}) executionContext.IfRequest(() => new GetByIdRequest<Customer, int> { Id = request.Id },
ifBeforePredicate: () => true,
ifAfterPredicate: response => response.Succeeded) executionContext.IfBreakRequest(() => new GetByIdRequest<Customer, int> { Id = request.Id },
ifBeforePredicate: () => true,
ifAfterPredicate: response => response.Succeeded)public class YourIntegrationCommand(ICommandExecutionContext executionContext)
: IntegrationCommand<YourIntegrationRequest, Customer>(executionContext)
{
public override async Task<BaseResponse<Customer>> Handle(YourIntegrationRequest request, CancellationToken cancellationToken) =>
await ExecutionContext
.CandidateGetByIdRequest(request.Dto.CandidateId)
.CustomerGetByIdRequest(request.Dto.CustomerIds)
.GetOtherStuffRequest(request.Dto.XYZType)
.PostSomethingRequest(request.Dto)
.SendMailRequest()
.Execute<Customer>(cancellationToken);
}