Jinaga.UnitTest
1.1.12
dotnet add package Jinaga.UnitTest --version 1.1.12
NuGet\Install-Package Jinaga.UnitTest -Version 1.1.12
<PackageReference Include="Jinaga.UnitTest" Version="1.1.12" />
<PackageVersion Include="Jinaga.UnitTest" Version="1.1.12" />
<PackageReference Include="Jinaga.UnitTest" />
paket add Jinaga.UnitTest --version 1.1.12
#r "nuget: Jinaga.UnitTest, 1.1.12"
#:package Jinaga.UnitTest@1.1.12
#addin nuget:?package=Jinaga.UnitTest&version=1.1.12
#tool nuget:?package=Jinaga.UnitTest&version=1.1.12
Jinaga.NET
Resilient, reliable data transfer for .NET.
What it Does
In Jinaga, you define a data model in terms of immutable facts. A fact represents an entity, a change to an entity, or a decision that a user or service has made.
In Jinaga.NET, facts are C# records.:
[FactType("Corporate.Company")]
record Company(string identifier) {}
[FactType("Corporate.Employee")]
record Employee(Company company, int employeeNumber) {}
When a user or service makes a decision, you will add a fact to the system. This will store it in the local database. It will also update the local UI. And it will publish it so that others can learn about the decision.
var contoso = await j.Fact(new Company("Contoso"));
var jane = await j.Fact(new Employee(contoso, 1));
var bob = await j.Fact(new Employee(contoso, 2));
To query facts, write a specification. Start at a know fact and find all related facts that match that specification.
var employeesOfCompany = Given<Company>.Match((company, facts) =>
from employee in facts.OfType<Employee>()
where employee.company == company
select employee
);
var contosoEmployees = await j.Query(contoso, employeesOfCompany);
A query returns results at a point in time. If you want to keep a user interface up-to-date, you will need to set up an event-based watch.
var observer = j.Watch(contoso, employeesOfCompany, employee =>
{
var component = AddEmployeeComponent(employee);
return () =>
{
RemoveEmployeeComponent(component);
};
});
Finally, if you want to be notified in real time of new information, just change Watch to Subscribe.
var subscription = j.Subscribe(contoso, employeesOfCompany, employee =>
{
var component = AddEmployeeComponent(employee);
return () =>
{
RemoveEmployeeComponent(component);
};
});
The client will open a persistent connection with the server. The server will notify the client the moment a new employee is hired. Because the client already set up a watch, the new employee will appear on the UI.
What a Result Promises
Query, Watch, and Subscribe all answer from the graph of facts that this replica holds.
Knowing what that answer promises, and what it does not, is what keeps a consumer from acting on information that has not settled.
- A result is exact for this replica's own graph. That graph is always a consistent state. A fact is never stored before its predecessors, a store saves each batch of facts together, and a read sees one snapshot, so no result is computed over half of a change.
- Replicas are peers. There is no global order and no authoritative answer. A result can differ from another replica's until the same facts have reached both, and then the two agree. A replicator is a peer with a graph of its own, so comparing a local watch against a query of the replicator compares two replicas. It does not check one of them against the truth.
- A negation is provisional.
A
notExistscondition is evaluated over the local graph. A row whose negative condition holds here may leave the result when a witness written elsewhere arrives.WatchandSubscribethen call that row's removal function, andQuery, run again, omits the row.
Record the Premise
For an action that must not run on a provisional negation, record the premise in the model. The fact that the handler writes names the facts it acted on. A concurrent contradiction is then visible in history, and a specification can find it.
Suppose a facility manager assigns an office to each employee who has not been terminated.
[FactType("Corporate.Facility")]
record Facility(User manager, Company company, string name) {}
[FactType("Corporate.Termination")]
record Termination(Employee employee) {}
[FactType("Corporate.OfficeAssignment")]
record OfficeAssignment(Facility facility, Employee employee, string roomNumber) {}
The decision reads a negation, so it is provisional.
var employeesToAssign = Given<Company>.Match((company, facts) =>
from employee in facts.OfType<Employee>()
where employee.company == company
where !facts.Any<Termination>(termination => termination.employee == employee)
select employee
);
The assignment names the employee that the decision acted on. That reference is the premise.
var assignment = await j.Fact(new OfficeAssignment(facility, employee, "101"));
A Termination written on another replica does not erase that assignment, and it does not need to.
Both facts are in history, so a specification finds the pairs that contradict one another.
var contradictedAssignments = Given<Facility>.Match((facility, facts) =>
from assignment in facts.OfType<OfficeAssignment>()
where assignment.facility == facility
from termination in facts.OfType<Termination>()
where termination.employee == assignment.employee
select assignment
);
A watch on that specification tells the facility manager which assignments to revisit. Without the premise, the contradiction would show up only as a row that quietly left a result.
To narrow who can make a decision, name that principal with an authorization rule. Only the facility's manager can write an assignment for that facility. That does not make the decision unique: the same manager on two devices can still write competing assignments. It leaves one principal, who can see each conflict in history and resolve it.
private static AuthorizationRules Authorization(AuthorizationRules r) => r
.Any<User>()
.Type<OfficeAssignment>(assignment => assignment.facility.manager)
;
Running a Replicator
A Jinaga front end connects to a device called a Replicator. The Jinaga Replicator is a single machine in a network. It stores and shares facts. To get started, create a Replicator of your very own using Docker.
docker pull jinaga/jinaga-replicator
docker create --name my-replicator -p8080:8080 jinaga/jinaga-replicator
docker start my-replicator
This creates and starts a new container called my-replicator.
The container is listening at port 8080 for commands.
Configure Jinaga to use the replicator:
var j = JinagaClient.Create(options =>
{
options.HttpEndpoint = new Uri("http://localhost:8080/jinaga");
});
Roadmap
Jinaga.NET is co-evolving with Jinaga.JS. Each of these projects has a front end and a back end. Either front end is intended to work with either back end. And the back ends are intended to interconnect to form a decision substrate.
Storage
Jiaga.NET currently has support for SQLite and memory storage. The next storage solution to implement is PostgreSQL to support Docker deployment. After that, the MS SQL Server implementation will support enterprise solutions that need to keep the journal transactionally consistent with the projection.
Release
Each packable project declares its own version as a <VersionPrefix> in its
.csproj. See Versioning.
To release a new version of the Jinaga.NET library, create a new release in the format yyyymmdd.i.
For example, 20240917.1.
All of the packages are versioned separately.
A package whose version has not changed is skipped on publish.
The packages will be published to NuGet.
To accomplish this from the command line, use the following gh command.
gh release create 20240917.1 --generate-notes
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.1 is compatible. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.1
- Jinaga (>= 1.1.55)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.1.12 | 121 | 11/1/2026 |
| 1.1.11 | 165 | 9/26/2026 |
| 1.1.8 | 141 | 8/29/2026 |
| 1.1.6 | 126 | 8/29/2026 |
| 1.1.4 | 406 | 3/13/2025 |
| 1.1.2 | 272 | 12/26/2024 |
| 1.0.0 | 326 | 7/16/2024 |
| 0.12.8 | 311 | 7/14/2024 |
| 0.12.7 | 325 | 7/14/2024 |
| 0.12.6 | 314 | 7/13/2024 |
| 0.12.5 | 319 | 7/7/2024 |
| 0.12.4 | 327 | 7/6/2024 |
| 0.12.3 | 319 | 7/3/2024 |
| 0.12.2 | 344 | 6/29/2024 |
| 0.12.1 | 277 | 6/9/2024 |
| 0.12.0 | 323 | 5/26/2024 |
| 0.11.18 | 301 | 5/18/2024 |
| 0.11.17 | 336 | 4/19/2024 |
| 0.11.16 | 328 | 4/14/2024 |
| 0.11.15 | 346 | 4/9/2024 |