Introduction
Flux is an abstraction between your .NET application and external services. It provides a simple way to interact with those services without depending directly on how communication is implemented.
Flux is designed to be:
- Easy to use — it provides common abstractions for interacting with external services.
- Configurable — it is highly configurable and can be easily adapted to your needs.
- Extendable — it provides implementation packages for different use-cases, while also allowing custom implementations.
- Testable — it lets application code depend on Flux abstractions, so you can mock Flux components when testing your application.
Proceed to the following sections to learn more about Flux implementations:
- Flux.REST - REST protocol implementation for Flux.
- Flux.JSON - Implements Flux via JSON, allowing easy Web API mocking by defining data as JSON strings.
Introduction
This guide shows how to connect your .NET application to a RESTful Web API using Flux.REST.
Installation
Install the Flux.REST NuGet package:
dotnet add package BitzArt.Flux.Rest
Get Started
Example Web API
Consider the following example Web API that manages courses and students. In Flux.REST, each resource is represented by an abstraction called Set.
Courses Set
| Operation | HTTP method | Request URL |
|---|---|---|
| Get Page | GET | https://school-api.example.com/courses?offset={offset}&limit={limit} |
| Get | GET | https://school-api.example.com/courses/{id} |
| Add | POST | https://school-api.example.com/courses |
| Update | PUT | https://school-api.example.com/courses/{id} |
| Remove | DELETE | https://school-api.example.com/courses/{id} |
{
"Id": 1,
"Title": "Introduction to Biology"
}
Students Set
| Operation | HTTP method | Request URL |
|---|---|---|
| Get | GET | https://school-api.example.com/students/{id} |
| Get Page | GET | https://school-api.example.com/courses/{courseId}/students?offset={offset}&limit={limit} |
{
"Id": 1,
"Name": "Alice"
}
Note
Notice that Students can be fetched by ID of a Course they participate in.
Configuration
Add respective model classes to match the API’s resources:
public class Course
{
public int? Id { get; set; }
public string? Title { get; set; }
}
public class Student
{
public int? Id { get; set; }
public string? Name { get; set; }
}
Configure Flux and the Course and Student Sets:
services.AddFlux(flux =>
{
flux.AddService("school-api")
.UsingRest("https://school-api.example.com")
.AddSet<Course, int>()
.WithEndpoint("courses")
.AddSet<Student, int>()
.WithEndpoint("students")
.WithGet((PageRequest _) => "courses/{{courseId}}/students");
});
AddFluxregisters the Flux Context in the DI container.AddService("school-api")registers a Flux Service namedschool-apiin the DI container.UsingRestconfigures the Service to use REST and sets its base URL.AddSet<Course, int>registers a set for the Course resourse.WithEndpoint("courses")configures"courses"base endpoint path for all Course Set operations.
AddSet<Student, int>registers a set for the Student resourse.WithEndpoint("students")configures"students"base endpoint path for all Student Set operations.WithGet((PageRequest _) => ...)sets the path for Get Page operation. It will take precedence overWithEndpoint("students")configuration for this operation.
Tip
For advanced endpoint configuration and precedence rules, see Endpoint Configuration.
Resolve Services
Inject Set Contexts to work with set data:
public class SchoolService(
IFluxSetContext<Course> courseSetContext,
IFluxSetContext<Student> studentSetContext)
{
}
Work with Sets
Courses
Use the injected courseSetContext to run Course operations:
// [GET] https://school-api.example.com/courses?offset=0&limit=20
var coursesPage = await courseSetContext.GetPageAsync(offset: 0, limit: 20);
// [GET] https://school-api.example.com/courses/{courseId}
var course = await courseSetContext.GetAsync(courseId);
// [POST] https://school-api.example.com/courses
await courseSetContext.AddAsync(newCourse);
// [DELETE] https://school-api.example.com/courses/{courseId}
await courseSetContext.RemoveAsync(courseId);
Flux.REST sends the corresponding HTTP requests shown in the Course endpoints table.
Students
The Student Get Page path contains {courseId}. Pass its value as a named operation parameter when you call studentSetContext.GetPageAsync:
var parameters = new OperationParameterCollection(
new List<KeyValuePair<string, object>>
{
new("courseId", 42)
});
// [GET] https://school-api.example.com/courses/42/students?offset=0&limit=20
var studentsPage = await studentSetContext.GetPageAsync(
offset: 0,
limit: 20,
parameters: parameters);
Pagination
Important
Configurable pagination is currently a work-in-progress.
Endpoint Configuration
Endpoint configuration determines which request path Flux.REST uses for each operation in a Set Context.
Configuration Precedence
When multiple configuration rules apply to a single endpoint, a rule with the most specific set of HTTP verbs and operational constraints will take precedence.
To demonstrate endpoint configuration precedence, suppose a school API uses separate paths for retrieving and managing students:
| HTTP method | Request path |
|---|---|
GET | /courses/{courseId}/students?offset={offset}&limit={limit} |
GET | /students/{id} |
POST | /management/students |
PUT | /management/students/{id} |
DELETE | /management/students/{id} |
Configure Flux
The Student Set has three endpoint configurations:
.AddSet<Student, int>()
.WithEndpoint("management/students")
.WithGet("students")
.WithGet((PageRequest _) => "courses/{{courseId}}/students")
WithGet("students")takes precedence overWithEndpoint("management/students")for the Get operation because it applies only toGETverb.WithGet((PageRequest _) => "courses/{courseId}/students")takes precedence overWithGet("students")because it applies specifically to the Get Page operation because of thePageRequestoperational constraint.WithEndpoint("management/students")specifies the base path, which is then used inAdd,Update, andRemoveoperations.
Tip
See the other available endpoint configuration method overloads.
Sample operations
Example operations that can be performed using the configuration as shown above:
| HTTP method | Request path | Configuration | Usage |
|---|---|---|---|
GET | /courses/{courseId}/students?offset={offset}&limit={limit} | .WithGet((PageRequest _) => "courses/{courseId}/students") | .GetPageAsync(offset: 0, limit: 20, parameters: parameters) |
GET | /students/{id} | .WithGet("students") | .GetAsync(42) |
POST | /management/students | .WithEndpoint("management/students") | .AddAsync(newStudent) |
PUT | /management/students/{id} | .WithEndpoint("management/students") | .UpdateAsync(updatedStudent, 42) |
DELETE | /management/students/{id} | .WithEndpoint("management/students") | .RemoveAsync(42) |
Operation Parameters
Operation parameters can be used to supply values for path placeholders and query parameters.
The following examples assume that the Student Get Page endpoint configuration contains a {{courseId}} path placeholder:
.WithGet((PageRequest _) => "courses/{{courseId}}/students")
Configuration as shown above can be used as follows:
var parameters = new OperationParameterCollection(
new[]
{
new KeyValuePair<string, object>("courseId", 42),
new KeyValuePair<string, object>("order", "name"),
new KeyValuePair<string, object>("desc", true)
});
// /courses/42/students?order=name&desc=True&offset=0&limit=20
var studentsPage = await studentSetContext.GetPageAsync(
offset: 0,
limit: 20,
parameters: parameters);
Parameter names are case-sensitive and must match path placeholders exactly. A named parameter collection must provide a value for every placeholder in the path.
Query Parameters
By default, Flux.REST automatically adds the following query parameters:
- named operation parameters that are not matched to path placeholders;
- non-null
offsetandlimitparameters for the Get Page operation.
To control this behavior, use the queryComplete argument. When queryComplete is true, Flux.REST replaces path placeholders but does not append any query parameters.
For example, suppose the school API expects skip and take instead of offset and limit. Build the complete query string from the operation parameters and page request:
.WithGet((GetPageOperationDescriptor operation) =>
{
var namedParameters = (INamedOperationParameterCollection)operation.Parameters!;
var order = namedParameters.Values.First(x => x.Key == "order").Value;
var desc = namedParameters.Values.First(x => x.Key == "desc").Value;
var skip = operation.PageRequest.Offset;
var take = operation.PageRequest.Limit;
return $"courses/{{courseId}}/students?order={order}&desc={desc}&skip={skip}&take={take}";
},
queryComplete: true)
The Student Get Page operation now translates to the following URL path:
/courses/42/students?order=name&desc=True&skip=0&take=20
HTTP Client Configuration
Flux.REST allows configuring an HttpClient for each Flux Service registered with UsingRest. The URL passed to UsingRest becomes the client’s BaseAddress.
Use ConfigureHttpClient to apply additional HttpClient settings for that Flux Service.
services.AddFlux(flux =>
{
flux.AddService("example-api")
.UsingRest("https://api.example.com")
.ConfigureHttpClient((HttpClient client) =>
{
// Configure the HttpClient here
});
});
The configuration applies to requests made through every Set belonging to the "example-api" Flux Service.
Tip
See the
HttpClientAPI reference for the available configuration options.
ConfigureHttpClient also has an overload that provides IServiceProvider when the configuration requires a registered dependency:
services.AddFlux(flux =>
{
flux.AddService("example-api")
.UsingRest("https://api.example.com")
.ConfigureHttpClient((IServiceProvider serviceProvider, HttpClient client) =>
{
// Configure the HttpClient here
});
});
JSON Serialization Configuration
Flux.REST uses System.Text.Json to serialize request bodies and deserialize response bodies.
Each Flux Service registered with UsingRest has its own JsonSerializerOptions. Use ConfigureJsonSerializer to configure those options:
services.AddFlux(flux =>
{
flux.AddService("example-api")
.UsingRest("https://api.example.com")
.ConfigureJsonSerializer((JsonSerializerOptions options) =>
{
// Configure the JsonSerializerOptions here
});
});
The configuration applies to JSON request and response bodies for every Set belonging to the "example-api" Flux Service.
Tip
See the
JsonSerializerOptionsAPI reference for the available options.
Request Interception
Flux.REST provides two ways to intercept outgoing requests and incoming responses: HttpClient message handlers and Flux.REST interceptors.
Both mechanisms are configured per Flux Service and apply to requests made through every Set belonging to that Service.
HTTP Message Handler
Create a custom HTTP message handler by deriving from DelegatingHandler and overriding SendAsync:
public class CustomHttpHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Implement custom logic here
return await base.SendAsync(request, cancellationToken);
}
}
Tip
See the outgoing request middleware documentation for more information about the
HttpClienthandler pipeline.
Register the handler in the DI container, then configure a Flux Service to use it with UsingRest<THandler>:
services.AddFlux(flux =>
{
flux.AddService("example-api")
.UsingRest<CustomHttpHandler>("https://api.example.com");
});
Flux.REST Interceptor
Create a custom Flux.REST interceptor by implementing IFluxRestInterceptor and its OnRequestAsync and OnResponseAsync methods:
public sealed class CustomInterceptor : IFluxRestInterceptor
{
public async Task OnRequestAsync(
HttpClient client,
HttpRequestMessage request,
CancellationToken cancellationToken)
{
// Implement custom logic here
}
public async Task OnResponseAsync(
HttpClient client,
HttpResponseMessage response,
CancellationToken cancellationToken)
{
// Implement custom logic here;
}
}
Configure a Flux Service to use it with WithInterceptor<TInterceptor>:
services.AddFlux(flux =>
{
flux.AddService("example-api")
.UsingRest("https://api.example.com")
.WithInterceptor<CustomInterceptor>();
});
Note
WithInterceptor<TInterceptor>registers the interceptor in the DI container with transient lifetime by default. Pass anotherServiceLifetimeas an argument when a different lifetime is required.
Introduction
When your application uses Flux to communicate with an external API, you can use Flux.JSON as an in-memory substitute for testing. Flux.JSON initializes Flux Set data from JSON strings or files, allowing the same application code to run without sending any real HTTP requests to the API.
Installation
Install the Flux.JSON NuGet package:
dotnet add package BitzArt.Flux.Json
Get Started
Consider the following example Web API that manages courses and students. In Flux.JSON, each resource is represented by an abstraction called Set.
Example Web API
Courses Set
| Operation | HTTP method | Request URL |
|---|---|---|
| Get Page | GET | https://school-api.example.com/courses?offset={offset}&limit={limit} |
| Get | GET | https://school-api.example.com/courses/{id} |
| Add | POST | https://school-api.example.com/courses |
| Update | PUT | https://school-api.example.com/courses/{id} |
| Remove | DELETE | https://school-api.example.com/courses/{id} |
{
"Id": 1,
"Title": "Introduction to Biology"
}
Students Set
| Operation | HTTP method | Request URL |
|---|---|---|
| Get | GET | https://school-api.example.com/students/{id} |
| Get Page | GET | https://school-api.example.com/courses/{courseId}/students?offset={offset}&limit={limit} |
{
"Id": 1,
"CourseIds": [1, 2],
"Name": "Alice"
}
Note
Notice that Students can be fetched by ID of a Course they participate in.
Configuration
Add respective model classes to match the API’s resources:
public class Course
{
public int? Id { get; set; }
public string? Title { get; set; }
}
public class Student
{
public int? Id { get; set; }
public List<int>? CourseIds { get; set; }
public string? Name { get; set; }
}
Add the initial course and student data as JSON arrays:
var coursesJson =
"""
[
{
"Id": 1,
"Title": "Introduction to Biology"
},
{
"Id": 2,
"Title": "Advanced Mathematics"
}
]
""";
var studentsJson =
"""
[
{
"Id": 1,
"CourseIds": [1],
"Name": "Alice"
},
{
"Id": 2,
"CourseIds": [1, 2],
"Name": "Ben"
},
{
"Id": 3,
"CourseIds": [2],
"Name": "Carla"
}
]
""";
Configure Flux and the Course and Student Sets:
services.AddFlux(flux =>
{
flux.AddService("school-api")
.UsingJson()
.AddSet<Course>()
.FromJson(coursesJson)
.WithKey(course => course.Id)
.AddSet<Student>()
.FromJson(studentsJson)
.WithKey(student => student.Id)
.EnrichQuery((query, operation) =>
{
if (operation is GetOperationDescriptor getOperation)
{
return query.Where(student => student.Id == (int?)getOperation.Id);
}
if (operation is GetPageOperationDescriptor &&
operation.Parameters is INamedOperationParameterCollection parameters)
{
var courseId = (int)parameters.Values.Single(parameter => parameter.Key == "courseId").Value;
return query.Where(student => student.CourseIds!.Contains(courseId));
}
return query;
});
});
AddFluxregisters the Flux Context in the DI container.AddService("school-api")registers a Flux Service namedschool-apiin the DI container.UsingJsonconfigures the Service to use Flux.JSON.AddSet<Course, int>registers a set for the Course resourse.FromJsonprovides the initial Set data from a JSON array.WithKeyselects the identifier of the model.AddSet<Student, int>registers a set for the Student resourse.EnrichQueryfilters read data based on the current operation and its parameters:- For the Get operation, the enriched query replaces the normal key lookup, so the
GetOperationDescriptorbranch filters data by the requested identifier. - For the Get Page operation,
EnrichQueryuses thecourseIdparameter to select only Students enrolled in the requested Course. Flux applies pagination after filtering the data.
- For the Get operation, the enriched query replaces the normal key lookup, so the
Load Data from Files
Use FromJsonFile when the initial data is stored in files. Pass a base directory to UsingJson and a file name to FromJsonFile. The following configuration loads Course data from data/courses.json:
services.AddFlux(flux =>
{
flux.AddService("school-data")
.UsingJson("data")
.AddSet<Course>()
.FromJsonFile("courses.json")
.WithKey(course => course.Id);
});
Resolve Services
Inject Set Contexts to work with set data:
public class SchoolService(
IFluxSetContext<Course> courseSetContext,
IFluxSetContext<Student> studentSetContext)
{
}
Work with Sets
Courses
Use the injected courseSetContext to run Course operations:
var coursesPage = await courseSetContext.GetPageAsync(offset: 0, limit: 20);
var course = await courseSetContext.GetAsync(courseId);
await courseSetContext.AddAsync(newCourse);
await courseSetContext.RemoveAsync(courseId);
Note
Add, update, and remove operations change the in-memory Course data but do not modify
coursesJson. Flux.JSON does not support partial updates.
Students
The Student Get Page path contains {courseId}. Pass its value as a named operation parameter when you call studentSetContext.GetPageAsync:
var parameters = new OperationParameterCollection(
new List<KeyValuePair<string, object>>
{
new("courseId", 1)
});
var studentsPage = await studentSetContext.GetPageAsync(
offset: 0,
limit: 20,
parameters: parameters);
The resulting page contains Alice and Ben, who are enrolled in Course 1.
JSON Serializer Configuration
Flux.JSON uses System.Text.Json to serialize and deserialize JSON data.
Each Flux Service registered with UsingJson has its own JsonSerializerOptions. Use ConfigureJsonSerializer to configure those options:
services.AddFlux(flux =>
{
flux.AddService("example-data")
.UsingJson()
.ConfigureJsonSerializer((JsonSerializerOptions options) =>
{
// Configure the JsonSerializerOptions here
});
});
The configuration applies to the initial data loaded for every Set belonging to the "example-data" Flux Service.
Tip
See the
JsonSerializerOptionsAPI reference for the available options.