Dynamically generates a generic CRUD API implementation backed with Entity Framework Core and Minimal API. This can be used to create a quick backend for prototyping apps that use CRUD operations.
- Nested entity permissions checking
- Roles based permissions with JWT and Cookie authentication support
- Quick API prototyping
- Small projects that only require CRUD functionality
- Frontend Testing (if a backend API is needed)
To install the Russkyc.MinimalApi.Framework package, you can use the NuGet Package Manager or the .NET CLI.
Follow these steps to set up the Russkyc.MinimalApi.Framework in your project.
- Create a new ASP.NET Core Web API project if you don't already have one.
- Install the
Russkyc.MinimalApi.FrameworkNuGet package using the cli or the nuget package manager - Install an EntityFramework Provider like
Microsoft.EntityFrameworkCore.InMemoryorMicrosoft.EntityFrameworkCore.Sqlitedepending on your database choice. - Add the required services, configuration, and mappings in the
Program.csfile:
There are two options for setting up the framework in your project, a minimal setup and a more granular standard setup.
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Russkyc.MinimalApi.Framework;
using Russkyc.MinimalApi.Framework.Core;
using Russkyc.MinimalApi.Framework.Core.Access;
using Russkyc.MinimalApi.Framework.Core.Attributes;
await MinimalApiFramework
.CreateDefault(options => options.UseSqlite("Data Source=test.sqlite"))
.RunAsync();
// Define your entity classes here or in a separate fileSee the Minimal Sample Project for the complete code.
using System.ComponentModel.DataAnnotations;
using Microsoft.EntityFrameworkCore;
using Russkyc.MinimalApi.Framework.Core;
using Russkyc.MinimalApi.Framework.Core.Access;
using Russkyc.MinimalApi.Framework.Core.Attributes;
using Russkyc.MinimalApi.Framework.Extensions;
using Russkyc.MinimalApi.Framework.Options;
var builder = WebApplication.CreateBuilder();
// Configure
FrameworkOptions.MapIndexToApiDocs = true;
FrameworkDbContextOptions.DbContextConfiguration = options => options.UseSqlite("Data Source=test.sqlite");
// Add required services
builder.Services
.AddMinimalApiFramework();
var webApplication = builder.Build();
// Add required endpoints
// Optionally, you can disable entity endpoints mapping and map them manually
webApplication.UseMinimalApiFramework(mapEntityEndpoints: false);
// If mapping is disabled, you can manually map entity endpoints
// Manual entity endpoints mapping for more granular control
webApplication.MapEntityEndpoints<SampleEntity, Guid>(options =>
{
// Other endpoint options can be configured here
});
// Sample prefixed mapping
// Same effect can be achieved when using the minimal setup
// by using the `FrameworkOptions` and setting `ApiPrefix`
var apiGroup = webApplication.MapGroup("nested");
apiGroup.MapEntityEndpoints<SampleEmbeddedEntity, int>();
await webApplication.RunAsync();
// Define your entity classes here or in a separate fileSee the Standard Sample Project for the complete code.
The framework provides several static options classes to customize its behavior.
You can set these options before your application startup in your Program.cs.
using System.Reflection;
public static class FrameworkOptions
{
public static Assembly? EntityClassesAssembly { get; set; } = null;
public static bool EnableRealtimeEvents { get; set; } = true;
public static bool MapIndexToApiDocs { get; set; } = true;
public static bool EnableApiDocs { get; set; } = true;
public static string? ApiPrefix { get; set; }
public static string PermissionHeader { get; set; } = "x-api-permission";
public static bool EnableRoleBasedPermissions { get; set; } = false;
public static bool EnableJwtAuthentication { get; set; } = false;
public static string? JwtIssuer { get; set; }
public static string? JwtAudience { get; set; }
public static string? JwtKey { get; set; }
public static bool EnableCookieAuthentication { get; set; } = false;
}Properties:
EntityClassesAssembly: The assembly containing your entity classes. (default:null)EnableRealtimeEvents: Enable or disable SignalR-based realtime events. (default:true)MapIndexToApiDocs: Map the root index to API docs. (default:true)EnableApiDocs: Enable or disable API documentation. (default:true)ApiPrefix: Set a custom API route prefix. (default:null)PermissionHeader: The HTTP header used for permission checks. (default:"x-api-permission")EnableRoleBasedPermissions: Enable role-based permissions using authenticated user roles. (default:false)EnableJwtAuthentication: Enable JWT bearer authentication. (default:false)JwtIssuer: The issuer for JWT validation. (default:null)JwtAudience: The audience for JWT validation. (default:null)JwtKey: The key for JWT signing and validation. (default:null)EnableCookieAuthentication: Enable cookie authentication. (default:false)
using Microsoft.EntityFrameworkCore;
using Russkyc.MinimalApi.Framework.Core;
public static class FrameworkDbContextOptions
{
public static Type? DbContextType { get; set; } = null;
public static ServiceLifetime DbContextLifetime { get; set; } = ServiceLifetime.Scoped;
public static ServiceLifetime DbContextOptionsLifetime { get; set; } = ServiceLifetime.Scoped;
public static Action<DbContextOptionsBuilder>? DbContextConfiguration { get; set; } = null;
public static DatabaseAction DatabaseAction { get; set; } = DatabaseAction.EnsureCreated;
}Properties:
DbContextType: CustomDbContexttype to use. (default:null)DbContextLifetime: Service lifetime for the DbContext. (default:Scoped)DbContextOptionsLifetime: Service lifetime for DbContext options. (default:Scoped)DbContextConfiguration: Action to further configure theDbContextOptionsBuilder. (default:null)DatabaseAction: Database initialization action (None,EnsureCreated,DeleteAndCreate). (default:EnsureCreated)
We can define a custom DbContext class that inherits from BaseDbContext. This can be configured using one of the properties in the FrameworkDbContextOptions class.
Sample Custom DbContext Class
using Microsoft.EntityFrameworkCore;
using Russkyc.MinimalApi.Framework;
namespace SampleProject;
public class CustomDbContext : BaseDbContext
{
public CustomDbContext(DbContextOptions options) : base(options)
{
}
// Entity collections are required to be defined
// using the naming convention `<ClassName>Collection`
public DbSet<SampleEmbeddedEntity> SampleEmbeddedEntityCollection { get; set; }
public DbSet<SampleEntity> SampleEntityCollection { get; set; }
}Configuration
FrameworkDbContextOptions.DbContextType = typeof(CustomDbContext);
FrameworkDbContextOptions.DatabaseAction = DatabaseAction.EnsureCreated;
FrameworkDbContextOptions.DbContextConfiguration = options =>
{
options.UseSqlite("Data Source=test.sqlite");
};using Scalar.AspNetCore;
public static class FrameworkApiDocsOptions
{
public static bool EnableSidebar { get; set; } = false;
public static ScalarLayout Layout { get; set; } = ScalarLayout.Classic;
public static ScalarTheme Theme { get; set; } = ScalarTheme.Default;
}Properties:
EnableSidebar: Show or hide the sidebar in API docs. (default:false)Layout: API docs layout (Classic, etc.), options areScalarLayout.ModernandScalarLayout.Classic. (default:ScalarLayout.Classic)Theme: API docs theme provided by scalar, options are available in theScalarThemeenum. (default:ScalarTheme.Default).
using Russkyc.MinimalApi.Framework.Core;
public static class FrameworkRealtimeOptions
{
public static string RealtimeEventsEndpoint { get; set; } = ConfigurationStrings.RealtimeHubEndpoint;
}Properties:
RealtimeEventsEndpoint: The SignalR endpoint for realtime events. (default:/crud-events)
Apart from the standard CRUD api functionality, there is also some support for advanced querying.
If you do a get request to the endpoint /api/sampleentity you will
receive a response that looks like this:
[
{
"id": 1,
"property": "Entity 1",
"embeddedEntity": null
},
{
"id": 2,
"property": "Entity 2",
"embeddedEntity": null
},
{
"id": 3,
"property": "Entity 3",
"embeddedEntity": null
},
{
"id": 4,
"property": "Entity 4",
"embeddedEntity": null
}
]This is because navigation properties for referenced entities are not
automatically included (for performance purposes). you can use the include
query parameter to include the referenced entity when needed.
GET /api/sampleentity?include=embeddedentityThen you will have this result:
[
{
"id": 1,
"property": "Entity 1",
"embeddedEntity": {
"id": 1,
"property2": "Embedded Entity 1"
}
},
{
"id": 2,
"property": "Entity 2",
"embeddedEntity": {
"id": 2,
"property2": "Embedded Entity 2"
}
},
{
"id": 3,
"property": "Entity 3",
"embeddedEntity": {
"id": 3,
"property2": "Embedded Entity 3"
}
},
{
"id": 4,
"property": "Entity 4",
"embeddedEntity": {
"id": 4,
"property2": "Embedded Entity 4"
}
}
]Entities can now be filtered with the filter queryParam and supports standard expressions. Here are a few
examples:
GET /api/sampleentity?filter=Content.StartsWith("hello")GET /api/sampleentity?filter=Content.StartsWith("hi") && !Content.Contains("user")GET /api/sampleentity?filter=Count == 1 || Count > 8GET /api/sampleentity?filter=ContactPerson != nullThese are visualized for readability, in actual use, the filter value should be URL Encoded.
Entities can be ordered using the orderBy and orderByDescending query parameters. Multiple properties can be specified for ordering, separated by commas. The first property will be ordered using OrderBy or OrderByDescending, and subsequent properties will be ordered using ThenBy or ThenByDescending.
GET /api/sampleentity?orderBy=property,embeddedEntity.property2&orderByDescending=truethe orderBy query param will define what properties are taken into consideration in ordering.
the orderByDescending query param is a bool property that changes the behavior to descending when set to true.
By default, pagination is disabled and the query collection response returns something like this
[
{
"id": 1,
"property": "Entity 1",
"embeddedEntity": {
"id": 1,
"property2": "Embedded Entity 1"
}
},
{
"id": 2,
"property": "Entity 2",
"embeddedEntity": {
"id": 2,
"property2": "Embedded Entity 2"
}
}
]To enable pagination, set the paginate query param to true
and set the page, pageSize query params as needed. as an example:
GET /api/sampleentity?paginate=true&page=1&pageSize=1This will now return a PaginatedCollection object with this JSON schema:
{
"data": [
{
"property": "Entity 1",
"embeddedEntity": null,
"id": "84e93f60-b2bc-4303-af0a-c51c205addb9"
}
],
"page": 1,
"pageSize": 1,
"totalRecords": 2,
"totalPages": 2
}Batch endpoints are supported for adding, updating, and deleting multiple entities at once.
POST /api/sampleentity/batch
Content-Type: application/json
[
{
"id": 1,
"property": "Entity 1",
"embeddedEntity": null
},
{
"id": 2,
"property": "Entity 2",
"embeddedEntity": null
}
]PUT /api/sampleentity/batch
Content-Type: application/json
[
{
"id": 1,
"property": "Updated Entity 1",
"embeddedEntity": null
},
{
"id": 2,
"property": "Updated Entity 2",
"embeddedEntity": null
}
]PATCH /api/sampleentity/batch?filter=@property.Contains("Old")
Content-Type: application/json
{
"property": "Updated Value"
}DELETE /api/sampleentity/batch?filter=@Count > 8Properties with data annotations such as [Required], [StringLength], and others will now be validated automatically
when creating entities. If validation fails, a 400 Bad Request response will be returned with the validation errors.
Example Class
using System.ComponentModel.DataAnnotations;
using Russkyc.MinimalApi.Framework.Core;
public class SampleEntity : DbEntity<Guid>
{
[Required, MinLength(5)]
public string Property { get; set; }
}When Validation fails, a 400 Bad Request response will be returned with this class as the response body:
Validation Error Class
public class ValidationError
{
public string Message { get; set; }
public IDictionary<string,string[]> Errors { get; set; }
}Note
Data validation is implemented using MiniValidation. Fluent validation interfaces are not supported.
The framework supports permission control at the entity level using the [RequirePermission] attribute. This allows you to restrict access to specific API methods (GET, POST, PUT, PATCH, DELETE) based on custom permissions or user roles.
To use role-based permissions or JWT/cookie authentication, configure the following options:
FrameworkOptions.EnableJwtAuthentication = true;
FrameworkOptions.EnableCookieAuthentication = true;
FrameworkOptions.EnableRoleBasedPermissions = true;
FrameworkOptions.JwtIssuer = "your-issuer";
FrameworkOptions.JwtAudience = "your-audience";
FrameworkOptions.JwtKey = "your-secret-key";EnableJwtAuthentication: Enables JWT bearer authentication.EnableCookieAuthentication: Enables cookie authentication.EnableRoleBasedPermissions: Enables checking user roles against required permissions.JwtIssuer,JwtAudience,JwtKey: Required for JWT validation.
To require permissions for certain API methods on an entity, decorate the entity class with the [RequirePermission] attribute. You can specify multiple permissions and apply the attribute multiple times for different methods.
Example:
using Russkyc.MinimalApi.Framework.Core.Access;
[RequirePermission(ApiMethod.Post, "create_permission")]
[RequirePermission(ApiMethod.Get, "read_permission")]
public class SampleEntity : DbEntity<Guid>
{
// ...properties...
}To require roles, use the [RequireRoles] attribute separately.
Example:
[RequireRoles(ApiMethod.Get, "Admin", "User")]
public class SampleEntity : DbEntity<Guid>
{
// ...properties...
}ApiMethod: The HTTP method to restrict (e.g.,ApiMethod.Get,ApiMethod.Post).permission: One or more permission strings required to access the endpoint.roles: One or more role names required to access the endpoint.
Permissions are checked from multiple sources in order of priority:
- HTTP Header: Custom permissions sent in the header defined by
FrameworkOptions.PermissionHeader(default:x-api-permission). - JWT Claims: Permissions from
permissionsorpermissionclaims in the JWT token. - User Roles: If role-based permissions are enabled, user roles from authentication (in cookies or in the JWT token) are checked.
Example request:
GET /api/sampleentity
x-api-permission: read_permissionYou can customize the header name by setting:
FrameworkOptions.PermissionHeader = "your-custom-header";When JWT or cookie authentication is enabled, permissions can also be extracted from the authenticated user's claims.
For JWT, include permissions in the token claims:
new Claim("permissions", "read_permission,write_permission")For cookies, permissions are stored in the authentication cookie.
If EnableRoleBasedPermissions is true, user roles are checked against the required permissions in [RequirePermission].
Example:
[RequirePermission(ApiMethod.Get, "Admin")]If the authenticated user has the "Admin" role, access is granted.
For direct role requirements, use [RequireRoles]:
Example:
[RequireRoles(ApiMethod.Get, "Admin")]Permissions are also checked for nested entities. If an entity has related entities with permission requirements, those are validated as well.
If multiple permissions are specified, the request must include at least one matching permission from any source.
Example:
[RequirePermission(ApiMethod.Get, "perm1", "perm2")]- Permission checks are only enforced for endpoints and methods decorated with
[RequirePermission]or[RequireRoles]. - If no such attributes are present, the endpoint is accessible without permission checks.
- You can apply multiple attributes to the same class for different methods.
