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.
- Scalar API Docs integration
- Shorter setup code for new projects learn more...
- Support for permission control in endpoints learn more...
- 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.Framework
NuGet package using the cli or the nuget package manager - Install an EntityFramework Provider like
Microsoft.EntityFrameworkCore.InMemory
orMicrosoft.EntityFrameworkCore.Sqlite
depending on your database choice. - Add the required services, configuration, and mappings in the
Program.cs
file:
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 file
See 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 file
See 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";
}
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"
)
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
: CustomDbContext
type 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.Modern
andScalarLayout.Classic
. (default:ScalarLayout.Classic
)Theme
: API docs theme provided by scalar, options are available in theScalarTheme
enum. (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=embeddedentity
Then 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 > 8
GET /api/sampleentity?filter=ContactPerson != null
These 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=true
the 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=1
This 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 > 8
Properties 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.
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;
using Russkyc.MinimalApi.Framework.Core.Attributes;
[RequirePermission(ApiMethod.Post, "create_permission")]
[RequirePermission(ApiMethod.Get, "read_permission")]
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.
When a request is made to an endpoint with permission control, the framework checks for the required permission(s) in the HTTP header defined by FrameworkOptions.PermissionHeader
(default: x-api-permission
).
If the required permission is not present in the header, the request will be rejected with a 403 Forbidden
response.
Example request:
GET /api/sampleentity
x-api-permission: read_permission
You can customize the header name by setting:
FrameworkOptions.PermissionHeader = "your-custom-header";
If multiple permissions are specified, the request must include at least one of the required permissions in the header value (comma-separated if multiple).
Example:
[RequirePermission(ApiMethod.Get, "perm1", "perm2")]
Request:
GET /api/sampleentity
x-api-permission: perm2
- Permission checks are only enforced for endpoints and methods decorated with
[RequirePermission]
. - If no
[RequirePermission]
attribute is present, the endpoint is accessible without permission checks. - You can apply multiple
[RequirePermission]
attributes to the same class for different methods.
Warning
Header-based permission control is a simple approach and should not be considered as secure as standard methods like JWT, Cookie, or Basic Authentication.
For public or production environments that require strong security, use .NET Authentication
/Authorization
with JWT or a custom authentication solution instead of static permission strings.
Realtime events are enabled by default and can be used to receive updates when entities are created, updated, or deleted and is implemented using SignalR.
We can enable or disable realtime events in the server by setting the EnableRealtimeEvents
property in the FrameworkOptions
class.
FrameworkOptions.EnableRealtimeEvents = true; // or false to disable
The default SignalR endpoint for realtime events is /realtime-events
, but you can change it by setting the RealtimeEventsEndpoint
property in the FrameworkRealtimeOptions
class.
FrameworkRealtimeOptions.RealtimeEventsEndpoint = "/api-events"; // Custom endpoint
using System.Text.Json;
using Microsoft.AspNetCore.SignalR.Client;
using Russkyc.MinimalApi.Framework.Core;
// ConfigurationStrings.RealtimeHubEndpoint provides the default SignalR endpoint for realtime events.
var connection = new HubConnectionBuilder()
.WithUrl($"https://localhost:7102{ConfigurationStrings.RealtimeHubEndpoint}", options =>
{
// If you have enabled permission control in some endpoints, you need to set the required permission
// in order to receive realtime events for those endpoints.
options.Headers.Add(ConfigurationStrings.ApiPermissionHeader, "xcxs");
})
.WithAutomaticReconnect()
.Build();
connection.On<RealtimeEvent>(ConfigurationStrings.RealtimeEvent, obj =>
{
var serialized = JsonSerializer.Serialize(obj, new JsonSerializerOptions()
{
WriteIndented = true
});
Console.WriteLine(serialized);
});
await connection.StartAsync();
Console.Read();
See the Minimal Client Sample Project for the complete code.
Each event returned is an EntityEvent<T>
type, where T is the type of data
returned by the resource event that triggered the websocket message. Eg; when
creating a SampleEntity
using post
the EntityEvent
being sent in realtime is of
type EntityEvent<SampleEntity>
.
public class EntityEvent<T>
{
public string Resource { get; set; }
public string Type { get; set; }
public T? Data { get; set; }
}
- The
Resource
property will be the name of the entity in lowercase, eg; "sampleentity". - The
Type
property will be the type of event, eg;created
,updated
,deleted
,batch-created
,batch-updated
,batch-deleted
- The
Data
property contains the data being returned by that resource method.
This is free and available for everyone to use, but still requires time for development and maintenance. By choosing to donate, you are not only helping develop this project, but you are also helping me dedicate more time for creating more tools that help the community β€οΈ
This project may exists with the help of these amazing open-source projects:
- Scalar - Used as the API documentation.
- System.Linq.Dynamic.Core - Used as the expression parser in advanced querying.
- MiniValidation - Used for data annotations validation.
- Entity Framework Core - For Database Access.
- SignalR - For the realtime events.
This project is made easier to develop by Jetbrains! They have provided Licenses to their IDE's to support development of this open-source project.