Sparc.Kernel
7.0.33
dotnet add package Sparc.Kernel --version 7.0.33
NuGet\Install-Package Sparc.Kernel -Version 7.0.33
<PackageReference Include="Sparc.Kernel" Version="7.0.33" />
paket add Sparc.Kernel --version 7.0.33
#r "nuget: Sparc.Kernel, 7.0.33"
// Install Sparc.Kernel as a Cake Addin #addin nuget:?package=Sparc.Kernel&version=7.0.33 // Install Sparc.Kernel as a Cake Tool #tool nuget:?package=Sparc.Kernel&version=7.0.33
Sparc.Features
The Sparc.Features
library is the main framework library for the Features Project in your Sparc solution.
What is a Features Project?
A Features Project is the core of your application, and arguably is your entire application. The UI platforms can be viewed as a plugin that enables a visible interface to access this project.
A Features Project is also the main back end API for your application.
This project should ideally strive to contain all of the application's logic, including:
- entities,
- operations on those entities,
- the entire API surface (including URL routes, dependencies, input and output data classes)
- all plugins (persistence, authentication, notification, etc.)
What is a Feature?
A Feature is a single operation that your app can perform, along with all of the necessary dependencies around that operation (persistence, authentication, notification, etc.).
Where did the idea of a Feature come from?
Almost all programming in the world can be abstracted down to the following formula:
In -> Modify() -> Out
Every function, every group of functions, every program, every project, every solution, is a long linear chain of this basic formula.
The closer that we can get to this basic formula, the simpler our architecture becomes.
So with that in mind, the basic ingredients of a Feature are the following:
In:
- The Name of the Feature
- The Input Data that the Feature needs
- The Dependencies that the Feature uses to do its job
Modify:
- The Logic that the Feature executes, using the Data and Dependencies to produce an Output
Out:
- The Result that the Feature spits out at the end of its job
What does a Feature look like?
// This is the form of your Input Data
public record GetOrderRequest(string CustomerId, string PurchaseOrderNumber);
// This is the form of your Output Data
public record GetOrderResponse(string OrderId, List<OrderDetail> Lines, decimal Tax, decimal Shipping);
// Inherit your class from Feature<InputType, OutputType> to enable all the goodness.
// Name your Feature well. It will become the permanent URL of your API (i.e. /api/GetOrder)
public class GetOrder : Feature<GetOrderRequest, GetOrderResponse>
{
// Inject your Dependencies into the constructor
public GetOrder(IRepository<Order> orders) => Orders = orders;
// Receive the Input Data as a parameter, use it in the body of the function,
// and return the Output Data as the result
public override async Task<GetOrderResponse> ExecuteAsync(GetOrderRequest request)
{
var order = Orders.Query.FirstOrDefault(o =>
o.CustomerId == request.CustomerId
&& o.PurchaseOrderNumber == request.PurchaseOrderNumber);
return new(order.Id, order.Lines, order.CalculateTax(), order.CalculateShipping());
}
}
What are the benefits of using Features?
Sparc.Kernel automatically turns every Feature you write into a separate API endpoint, with its own URL and full authentication support, with no additional configuration needed on your part.
When the project containing the example Feature above is built, Sparc.Kernel automatically creates the following:
- A protected API endpoint at
/api/GetOrder
- An auto-generated client class with a method for each Feature, which automatically calls the API at the correct URL and with the correct authentication headers:
public async Task<GetOrderResponse> GetOrderAsync(GetOrderRequest request);
How do I call a Feature from my UI/Web/Mobile/Desktop project?
- Inject the auto-generated Api class into your Blazor page/component (you can also do this once for the entire application in the Imports.razor file):
@inject PointOfSaleApi Api
- Call the appropriate method on the Api class (note: it will be named
[FeatureName]Async
):var request = new(customerId, poNum); var order = await Api.GetOrderAsync(request);
Get Started with a Features Project
Create a new ASP.NET Core Empty project (preferably called [YourProject].Features).
Add the
Sparc.Features
Nuget package to your newly created project:Add the following setting to your
appsettings.json
file, using the local URL and port of your Web project:{ "WebClientUrl": "https://localhost:7147" }
(Alternatively, you may pass the URL directly as a string in the Startup code below, but we prefer to keep it in
appsettings.json
, since it will change once deployed.)Add the following two lines of code to your
Startup.cs
file, in the appropriate methods:public void ConfigureServices(IServiceCollection services) { // Add this line of code services.Sparcify<Startup>(Configuration["WebClientUrl"]); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // Add this line of code app.Sparcify<Startup>(env); }
Create your Entities and Features.
FAQ
Can I create multiple Features per file, like MVC Controllers do?
Each feature is self-contained into a single class for a reason. A class is a great container for all of the things a Feature needs:
- A name (the class name)
- An input and output data format (the one-line records above the class are a nice in-file representation of these)
- A set of dependencies (automatically injected into the class constructor by the framework)
- A single function to execute that uses all the other ingredients (the overridden
ExecuteAsync()
function inherited from theFeature
class)
In addition, one Feature / one class per file ensures that all of the logic to execute that Feature resides in a single place. This is contrary to the more typical layered approach with separate repositories, managers, controllers, actions, but has proven to be a creative and organizational catalyst, as it enables pure focus on the logic that you are working on at the time, rather than having to hunt all over the code for the stack of functions that are executed.
Why do you use Records for your Input and Output data?
C# Records are a great way to create a separate data type that is:
- only used once,
- has no behavior, and
- are never mutated.
This happens to be the very definition of a well-constructed DTO (Data Transformation Object).
Since a Feature's Input and Output data classes meet all of these requirements, we can take advantage of a simple one-line construct to create these types, eliminating most of the boilerplate ceremony:
public record GetProductsRequest(string SearchTerm, bool ShowDeletedProducts);
It is a good practice to return a specific data type for the specific API endpoint you are calling, and to receive a specific data type into the API endpoint, rather than using the Entities or shared data types directly. This is the case even when the API needs to return something very close to the Entity itself. Specific data types per API endpoint enable the following benefits:
- you're protected from accidentally exposing more information than you wanted to (eg. a secure User ID in an entity),
- the core Entities are protected from needing to evolve as the API evolves,
- the shape of each API can evolve separately from all the Entities or other API methods
If you don't like records, you can use classes!
What if my Feature doesn't have any Input Data?
If you need to call an API method that only returns Output Data without any Input Data required, inherit your Feature from Feature<[your output data type]>
instead. This enables the following changes to your Feature:
The
ExecuteAsync
function has no input parameters:public override async Task<GetAllOrdersResponse> ExecuteAsync() {}
The client API will also automatically take no parameters:
var orders = await Api.GetAllOrdersAsync();
What if my Feature doesn't have any Output Data?
It is our opinion that all Features should have some form of Output Data, so that the projects using this API can know the result of their call. This can be as simple
as a bool
or ActionResult
return if you like, but in most cases there is always something slightly more substantial that can be returned. There is currently
no Feature type in Sparc.Kernel that returns no Output Data.
How do I authenticate my Features?
All Features inheriting from Feature<TIn, TOut>
or Feature<TOut>
are automatically authenticated with the [Authorize]
attribute of ASP.NET Core. This is a
design decision made on purpose, as most API endpoints in the real world should be private and authenticated.
The simplest way to set up authentication in your Sparc solution is to use one of Sparc's authentication plugins (Azure AD B2C, Active Directory, or Self-Hosted).
However, Sparc.Kernel also contains a non-authenticated Feature option called PublicFeature
. Public Features are useful for
true public API endpoints (such as a community-accessible list for non-logged-in users).
To make a feature public, simply inherit from PublicFeature<TIn, TOut>
or PublicFeature<TOut>
rather than Feature<TIn, TOut>
or Feature<TOut>
.
Public Features are also useful in the initial stages of building your app, before any authentication is set up.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net7.0
- Ardalis.ApiEndpoints (>= 4.0.1)
- Ardalis.Specification (>= 6.1.0)
- MediatR.Extensions.Microsoft.DependencyInjection (>= 11.0.0)
- Microsoft.AspNetCore.Authentication.JwtBearer (>= 7.0.0)
- Microsoft.EntityFrameworkCore (>= 7.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0.0)
- Sparc.Core (>= 7.0.2)
- Swashbuckle.AspNetCore (>= 6.5.0)
- Swashbuckle.AspNetCore.Annotations (>= 6.4.0)
- System.Collections (>= 4.3.0)
- System.IO.FileSystem.Primitives (>= 4.3.0)
- System.Linq.Dynamic.Core (>= 1.2.22)
- System.Text.Json (>= 7.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.