nostify 1.11.0
See the version list below for details.
dotnet add package nostify --version 1.11.0
NuGet\Install-Package nostify -Version 1.11.0
<PackageReference Include="nostify" Version="1.11.0" />
<PackageVersion Include="nostify" Version="1.11.0" />
<PackageReference Include="nostify" />
paket add nostify --version 1.11.0
#r "nuget: nostify, 1.11.0"
#addin nuget:?package=nostify&version=1.11.0
#tool nuget:?package=nostify&version=1.11.0
nostify
Dirtball simple, easy to use, HIGHLY opinionated .Net framework for Azure to spin up microservices that can scale to global levels.
"Whole ass one thing, don't half ass two things" - Ron Swanson
This framework is intended to simplify the implementation of the ES/CQRS, microservice, and materialzed view patterns in a specific tech stack. It also assumes some basic familiarity with domain driven design.
When should I NOT use this? The framework makes numerous assumptions to cut down on complexity. It has dependencies on Azure components, notably Cosmos. If you need to accomodate a wide range of possible technology stacks, or want lots of flexibility in how to implement, this may not be for you. If you are going to ignore the tech stack requirements there are other libraries you should look at.
When should I use this? You should consider using this if you are using .Net and Azure and want to follow a strong set of guidelines to quickly and easily spin up services that can massively scale without spending tons of time architechting it yourself.
Current Status
- Brought Kafka into the mix
- Documentation still in process below!
Getting Started
To run locally you will need to install some dependencies:
- Azurite: npm install azurite
- Azurite VS Code Extension: https://marketplace.visualstudio.com/items?itemName=Azurite.azurite
- Docker Desktop: https://www.docker.com/products/docker-desktop/
- Confluent CLI: https://docs.confluent.io/confluent-cli/current/install.html
- Cosmos Emulator: https://learn.microsoft.com/en-us/azure/cosmos-db/how-to-develop-emulator?tabs=windows%2Ccsharp&pivots=api-nosql
To spin up a nostify project:
dotnet new install nostify
dotnet new nostify -ag <Your_Aggregate_Name> -p <Port Number To Run on Locally>
dotnet restore
This will install the templates, create the default project based off your Aggregate, and install all the necessary libraries.
Architecture
The library is designed to be used in a microservice pattern (although not necessarily required) using an Azure Function App api and Cosmos as the event store. Kafka serves as the messaging backpane, and projections can be stored in Cosmos or Redis depending on query needs.
You should set up a Function App and Cosmos per Aggregate Microservice.
Projections that contain data from multiple Aggregates can be updated by Event Handlers from other microservices. Why would this happen? Well say you have a Bank Account record. If we were using a relational database for a data store we'd have to either run two queries or do a join to get the Bank Account and the name of the Account Manager. Using the CQRS model, we can "pre-render" a projection that contains both the account info and the account manager info without having to join tables together. This example is obviously very simple, but in a complex environment where you're joining together dozens of tables to create a DTO to send to the user interface and returning 100's of thousands or millions of records, this type of archtecture can dramatically improve BOTH system performance and throughput.
Why????
When is comes to scaling there are two things to consider: speed and throughput. "Speed" meaning the quickness of the individual action, and "throughput" meaning the number of concurrent actions that can be performed at the same time. Using nostify addresses both of those concerns.
Speed really comes into play only on the query side for most applications. Thats a large part of the concept behind the CQRS pattern. By seperating the command side from the query side you essentially deconstruct the datastore that would traditionally be utilizing a RDBMS in order to create materialized views of various projections of the aggregate. Think of these views as "pre-rendered" views in a traditional relational database. In a traditional database a view simplifies queries but still runs the joins in real time when data is requested. By materializing the view, we denormalize the data and accept the increased complexity associated with keeping the data accurate in order to massively decrease the performance cost of querying that data. In addition, we gain flexibility by being able to appropriately resource each container to give containers being queried the hardest more resources.
Throughput is the other half of the equation. If you were using physical architechture, you'd have an app server talking to a seperate database server serving up your application. The app server say has 4 processors with 8 cores each, so there is a limitation on the number of concurrent tasks that can be performed. We can enhance throughput through proper coding, using parallel processing, and non-blocking code, but there is at a certain point a physical limit to the number of things that can be happening at once. With nostify and the use of Azure Functions, this limitation is removed other than by cost. If 1000 queries hit at the same moment in time, 1000 instances of an Azure Function spin up to handle it. You're limited more by cost than physical hardware.
Setup
The template will use dependency injection to add a singleton instance of the Nostify class and adds HttpClient by default. You may need to edit these to match your configuration:<br/>
public class Program
{
private static void Main(string[] args)
{
var host = new HostBuilder()
.ConfigureFunctionsWorkerDefaults()
.ConfigureServices((context, services) =>
{
services.AddHttpClient();
var config = context.Configuration;
//Note: This is the api key for the cosmos emulator by default
string apiKey = config.GetValue<string>("apiKey");
string dbName = config.GetValue<string>("dbName");
string endPoint = config.GetValue<string>("endPoint");
string kafka = config.GetValue<string>("BrokerList");
string aggregateRootCurrentStateContainer = "SiteCurrentState";
var nostify = new Nostify(apiKey, dbName, endPoint, kafka, aggregateRootCurrentStateContainer);
services.AddSingleton<INostify>(nostify);
services.AddLogging();
})
.Build();
host.Run();
}
}
By default, the template will contain the single Aggregate specified. In the Aggregates folder you will find Aggregate and AggregateCommand class files already stubbed out. The AggregateCommand base class contains default implementations for Create, Update, and Delete. The UpdateProperties<T>()
method will update any properties of the Aggregate with the value of the Event payload with the same property name.
public class TestCommand : NostifyCommand
{
///<summary>
///Base Create Command
///</summary>
public static readonly TestCommand Create = new TestCommand("Create_Test", true);
///<summary>
///Base Update Command
///</summary>
public static readonly TestCommand Update = new TestCommand("Update_Test");
///<summary>
///Base Delete Command
///</summary>
public static readonly TestCommand Delete = new TestCommand("Delete_Test");
public TestCommand(string name, bool isNew = false)
: base(name, isNew)
{
}
}
public class Test : NostifyObject, IAggregate
{
public Test()
{
}
public bool isDeleted { get; set; } = false;
public static string aggregateType => "Test";
public override void Apply(Event eventToApply)
{
if (eventToApply.command == TestCommand.Create || eventToApply.command == TestCommand.Update)
{
this.UpdateProperties<Test>(eventToApply.payload);
}
else if (eventToApply.command == TestCommand.Delete)
{
this.isDeleted = true;
}
}
}
Basic Tasks
Initializing Current State Container
The template will include a basic method to create, or recreate the current state container. It might become necesseary to recreate a container if a bug was introduced that corrupted the data, for instance. For the current state of an Aggreate, it is simple to recreate the container from the event stream. You will find the function to do so under Admin.
Querying
There is a Queries folder to contain the queries for the Aggregate. Three basic queries are created when you spin up the template: Get Single GET <AggreateType>/{aggregateId}
, Get All GET <AggregateType>
(note: if this will return large amounts of data you may want to refactor the default query), and Rehydrate GET Rehydrate<AggregateType>/{aggregateId}/{datetime?}
which returns the current state of the Aggregate directly from the event stream to the specified datetime.
To do your own query, simply add a new Azure Function per query, inject HttpClient
and INostify
, grab the container you want to query, and run a query with GetItemLinqQueryable<T>()
using Linq syntax.
public class GetTest
{
private readonly HttpClient _client;
private readonly INostify _nostify;
public GetTest(HttpClient httpClient, INostify nostify)
{
this._client = httpClient;
this._nostify = nostify;
}
[Function(nameof(GetTest))]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "Test/{aggregateId}")] HttpRequestData req,
Guid aggregateId,
ILogger log)
{
Container currentStateContainer = await _nostify.GetCurrentStateContainerAsync();
Test retObj = await currentStateContainer
.GetItemLinqQueryable<Test>()
.Where(x => x.id == aggregateId)
.FirstOrDefaultAsync();
return new OkObjectResult(retObj);
}
}
<strong>Example Repo Walkthrough</strong>
In the example repo you will find a simple BankAccount example. We will walk through it below. First create a directory for your example. Navigate to the location you'd like to keep the code and from powershell or cmd:
mkdir Nostify_Example
cd .\Nostify_Example
mkdir BankAccount
cd .\BankAccount
dotnet new nostify -ag BankAccount
dotnet restore
This will create a project folder to hold all of your microservices, and a folder for your BankAccount service.
If we will look at the BankAccount.cs file in the Aggregate folder that was created by the cli, we'll see it contains two classes which form the basis of everything we will do with this service: BankAccountCommand and BankAccount. BankAccountCommand implements the AggregateCommand class which already defines the Create, Update, and Delete commands which will be needed in the vast majority of scenarios. However, you need to define the commands/events beyond that. The other is the base BankAccount which has a rudametary Apply()
method and implements the Aggregate abstract class, which adds a few basic properties you will need to define and implements the NostifyObject abstract class which gives you the UpdateProperties<T>()
method.
First we will go in and add some basic properties to BankAccount and add a Transaction class to define a bank account transaction:
public class BankAccount : Aggregate
{
public BankAccount()
{
this.transactions = new List<Transaction>();
}
public int accountId { get; set; }
public Guid accountManagerId { get; set; }
public string customerName { get; set; }
public List<Transaction> transactions { get; set; }
new public static string aggregateType => "BankAccount";
public override void Apply(Event pe)
{
if (pe.command == AggregateCommand.Create || pe.command == AggregateCommand.Update)
{
this.UpdateProperties<BankAccount>(pe.payload);
}
else if (pe.command == AggregateCommand.Delete)
{
this.isDeleted = true;
}
}
}
public class Transaction
{
public decimal amount { get; set; }
}
Now we can take a look at adding custom commands. Create, Update, and Delete are already registered inside the base class so we don't need to add them. However, a bank account might need to process a Transaction for example, so we add the definition in the BankAccountCommand class. This registers the command with nostify to allow you to handle it in Apply()
:<br/>
public class BankAccountCommand : NostifyCommand
{
public static readonly BankAccountCommand ProcessTransaction = new BankAccountCommand("Process Transaction");
public BankAccountCommand(string name)
: base(name)
{
}
}
Then we add a handler for it in the Apply()
method:
public override void Apply(Event pe)
{
if (pe.command == BankAccountCommand.Create || pe.command == BankAccountCommand.Update)
{
this.UpdateProperties<BankAccount>(pe.payload);
}
else if (pe.command == BankAccountCommand.ProcessTransaction)
{
Transaction transaction = ((JObject)pe.payload).ToObject<Transaction>();
this.transactions.Add(transaction);
}
else if (pe.command == BankAccountCommand.Delete)
{
this.isDeleted = true;
}
}
If you have numerous custom commands and the if-else tree gets complex, it can be refactored into a switch statement or a Dictionary<AggregateCommand, Action>()
for easier maintinance.
<br/>
<strong>Command Functions</strong><br/>
Commands now become easy to compose. Using the nostify cli results in Create, Update, and Delete commands being stubbed in. In this simplified code we don't do any checking to see if the account already exists or any other validation you would do in a real app. All that is required is to instantiate an instance of the Event
class and call PersistAsync()
to write the event to the event store.
<br/>
public class CreateAccount
{
private readonly HttpClient _client;
private readonly Nostify _nostify;
public CreateAccount(HttpClient httpClient, Nostify nostify)
{
this._client = httpClient;
this._nostify = nostify;
}
[FunctionName("CreateAccount")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] BankAccount account, HttpRequest httpRequest,
ILogger log)
{
var peContainer = await _nostify.GetEventsContainerAsync();
Guid aggId = Guid.NewGuid();
account.id = aggId;
Event pe = new Event(NostifyCommand.Create, account.id, account);
await _nostify.PersistAsync(pe);
return new OkObjectResult(new{ message = $"Account {account.id} for {account.customerName} was created"});
}
}
The standard Update command is also very simple. You shouldn't have to modify much if at all. It accepts a dynamic
object so you can pass an object from the front end that contains only the properties that are being updated. This is handy when you may have multiple users updating the same aggregate at the same time and don't want to overwrite changes by passing the entire object. Nostify will match the property to on that exists on the Aggregate Root and update that in the Apply()
method. The default implementation will then update the currentState
container.
<br/>
public class UpdateBankAccount
{
private readonly HttpClient _client;
private readonly Nostify _nostify;
public UpdateBankAccount(HttpClient httpClient, Nostify nostify)
{
this._client = httpClient;
this._nostify = nostify;
}
[FunctionName("UpdateBankAccount")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] dynamic upd, HttpRequest httpRequest,
ILogger log)
{
Guid aggRootId = Guid.Parse(upd.id.ToString());
Event pe = new Event(NostifyCommand.Update, aggRootId, upd);
await _nostify.PersistAsync(pe);
return new OkObjectResult(new{ message = $"Account {upd.id} was updated"});
}
}
<br/> Custom AggregateCommands are composed the same way. In the Commands folder, add a new ProcessTransaction.cs file. Add the code below to allow a post with a couple of query parameters to add a transaction to a BankAccount:<br/>
public class ProcessTransaction
{
private readonly HttpClient _client;
private readonly Nostify _nostify;
public ProcessTransaction(HttpClient httpClient, Nostify nostify)
{
this._client = httpClient;
this._nostify = nostify;
}
[FunctionName("ProcessTransaction")]
public async Task Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequest req,
ILogger log)
{
Guid accountId = Guid.Parse(req.Query["id"]);
decimal amt = decimal.Parse(req.Query["amount"]);
var trans = new Transaction()
{
amount = amt
};
AggregateCommand command = BankAccountCommand.AddTransaction;
Event pe = new Event(command, accountId, trans);
await _nostify.PersistAsync(pe);
}
}
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net8.0 is compatible. 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. |
-
net8.0
- Confluent.Kafka (>= 2.3.0)
- Microsoft.AspNetCore.Mvc (>= 2.2.0)
- microsoft.azure.cosmos (>= 3.38.1)
- Microsoft.Azure.Functions.Worker (>= 1.21.0)
- Moq (>= 4.20.69)
- Newtonsoft.Json (>= 13.0.3)
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 |
---|---|---|
2.9.2 | 796 | 2/25/2025 |
2.9.1 | 94 | 2/25/2025 |
2.9.0 | 891 | 2/10/2025 |
2.8.0 | 160 | 2/4/2025 |
2.7.0 | 123 | 1/22/2025 |
2.6.3 | 211 | 1/7/2025 |
2.6.2 | 108 | 1/7/2025 |
2.6.1 | 155 | 12/31/2024 |
2.6.0 | 96 | 12/30/2024 |
2.5.0 | 114 | 12/27/2024 |
2.4.2 | 132 | 12/16/2024 |
2.4.1 | 96 | 12/16/2024 |
2.4.0 | 109 | 12/16/2024 |
2.3.0 | 185 | 12/13/2024 |
2.2.3 | 100 | 12/13/2024 |
2.2.2.1 | 113 | 12/11/2024 |
2.2.2 | 102 | 12/11/2024 |
2.2.1 | 102 | 12/8/2024 |
2.2.0 | 100 | 12/8/2024 |
2.1.0.1 | 107 | 12/6/2024 |
2.1.0 | 117 | 12/5/2024 |
2.0.0.10-alpha | 79 | 12/2/2024 |
2.0.0.9-alpha | 380 | 11/26/2024 |
2.0.0.8-alpha | 92 | 11/22/2024 |
2.0.0.7-alpha | 100 | 11/21/2024 |
2.0.0.6-alpha | 87 | 11/20/2024 |
2.0.0.5-alpha | 76 | 11/20/2024 |
2.0.0.4-alpha | 80 | 11/20/2024 |
2.0.0.3-alpha | 85 | 11/20/2024 |
2.0.0.2-alpha | 81 | 11/20/2024 |
2.0.0.1-alpha | 74 | 11/19/2024 |
2.0.0 | 161 | 12/3/2024 |
2.0.0-alpha | 153 | 11/18/2024 |
1.20.2 | 286 | 10/2/2024 |
1.20.1 | 144 | 8/13/2024 |
1.20.0 | 134 | 8/13/2024 |
1.19.1 | 100 | 7/23/2024 |
1.19.0 | 96 | 7/23/2024 |
1.18.0 | 110 | 7/23/2024 |
1.17.0 | 100 | 7/22/2024 |
1.16.0 | 107 | 7/22/2024 |
1.15.0 | 111 | 7/19/2024 |
1.14.2.2 | 98 | 7/19/2024 |
1.14.2.1 | 163 | 7/19/2024 |
1.14.2 | 114 | 7/15/2024 |
1.14.1 | 115 | 7/15/2024 |
1.14.0.2 | 116 | 5/3/2024 |
1.14.0.1 | 90 | 5/3/2024 |
1.13.0 | 89 | 5/2/2024 |
1.12.4.4 | 103 | 5/1/2024 |
1.12.4.3 | 98 | 5/1/2024 |
1.12.4.2 | 141 | 4/29/2024 |
1.12.4 | 129 | 4/29/2024 |
1.12.3.1 | 137 | 3/22/2024 |
1.12.3 | 124 | 3/21/2024 |
1.12.2 | 122 | 3/21/2024 |
1.12.1 | 127 | 3/20/2024 |
1.12.0 | 121 | 3/20/2024 |
1.11.0.2 | 128 | 3/20/2024 |
1.11.0.1 | 129 | 3/20/2024 |
1.11.0 | 130 | 3/20/2024 |
1.10.0 | 137 | 2/29/2024 |
1.9.1.3 | 245 | 12/1/2023 |
1.9.1.2 | 159 | 11/30/2023 |
1.9.1.1 | 145 | 11/30/2023 |
1.9.0.9 | 153 | 11/29/2023 |
1.9.0.8 | 152 | 11/28/2023 |
1.9.0.7 | 154 | 11/28/2023 |
1.9.0.6 | 145 | 11/28/2023 |
1.9.0.5 | 139 | 11/28/2023 |
1.9.0.4 | 138 | 11/28/2023 |
1.9.0.3 | 148 | 11/28/2023 |
1.9.0.2 | 142 | 11/28/2023 |
1.9.0.1 | 137 | 11/27/2023 |
1.9.0 | 140 | 11/27/2023 |
1.8.9.6 | 134 | 11/21/2023 |
1.8.9.5 | 138 | 11/21/2023 |
1.8.9.4 | 130 | 11/10/2023 |
1.8.9.3 | 148 | 11/4/2023 |
1.8.9.2 | 143 | 11/4/2023 |
1.8.9.1 | 125 | 11/3/2023 |
1.8.9 | 120 | 11/3/2023 |
1.8.8.1 | 146 | 11/2/2023 |
1.8.8 | 144 | 11/2/2023 |
1.8.7.1 | 147 | 11/1/2023 |
1.8.7 | 149 | 10/27/2023 |
1.8.6 | 165 | 10/17/2023 |
1.8.5.3 | 146 | 10/16/2023 |
1.8.5.2 | 164 | 10/16/2023 |
1.8.5.1 | 155 | 10/6/2023 |
1.8.5 | 161 | 10/6/2023 |
1.8.4 | 152 | 10/6/2023 |
1.8.3 | 164 | 10/5/2023 |
1.8.2 | 161 | 10/5/2023 |
1.8.1 | 157 | 10/5/2023 |
1.8.0 | 295 | 2/18/2023 |
1.7.11 | 463 | 5/24/2022 |
1.7.10 | 433 | 5/24/2022 |
1.7.9 | 433 | 5/23/2022 |
1.7.8 | 444 | 5/19/2022 |
1.7.7 | 443 | 5/18/2022 |
1.7.6 | 442 | 5/18/2022 |
1.7.5 | 494 | 4/20/2022 |
1.7.4 | 458 | 4/20/2022 |
1.7.3 | 454 | 4/20/2022 |
1.7.2 | 473 | 4/19/2022 |
1.7.1 | 469 | 4/19/2022 |
1.7.0 | 454 | 3/24/2022 |
1.6.6 | 390 | 8/10/2021 |
1.6.5 | 364 | 8/10/2021 |
1.6.4 | 353 | 8/10/2021 |
1.6.3 | 414 | 6/29/2021 |
1.6.2 | 402 | 6/29/2021 |
1.6.1 | 492 | 6/25/2021 |
1.6.0 | 383 | 6/23/2021 |
1.5.10 | 380 | 6/23/2021 |
1.5.9 | 366 | 2/17/2021 |
1.5.8 | 374 | 2/17/2021 |
1.5.7 | 376 | 2/16/2021 |
1.5.6 | 359 | 2/15/2021 |
1.5.5 | 361 | 2/15/2021 |
1.5.3 | 380 | 2/15/2021 |
1.5.2 | 373 | 2/15/2021 |
1.5.1 | 357 | 2/15/2021 |
1.5.0 | 358 | 2/15/2021 |
1.4.9 | 367 | 2/15/2021 |
1.4.8 | 381 | 2/15/2021 |
1.4.7 | 381 | 2/15/2021 |
1.4.5 | 373 | 2/15/2021 |
1.4.4 | 385 | 2/15/2021 |
1.4.3 | 364 | 2/15/2021 |
1.4.2 | 368 | 2/15/2021 |
1.4.1 | 373 | 2/15/2021 |
1.4.0 | 404 | 2/13/2021 |
1.3.6 | 411 | 2/13/2021 |
1.3.3 | 388 | 2/10/2021 |
1.3.2 | 376 | 2/10/2021 |
1.3.1 | 441 | 2/9/2021 |
1.3.0 | 411 | 2/9/2021 |
1.2.16 | 393 | 2/8/2021 |
1.2.15 | 404 | 2/8/2021 |
1.2.14 | 401 | 2/8/2021 |
1.2.13 | 407 | 2/8/2021 |
1.2.12 | 397 | 1/29/2021 |
1.2.11 | 384 | 1/29/2021 |
1.2.10 | 353 | 1/29/2021 |
1.2.9 | 373 | 1/29/2021 |
1.2.8 | 421 | 1/28/2021 |
1.2.7 | 390 | 1/28/2021 |
1.2.6 | 396 | 1/28/2021 |
1.2.5 | 405 | 1/28/2021 |
1.2.4 | 426 | 1/27/2021 |
1.2.3 | 429 | 1/27/2021 |
1.2.2 | 420 | 1/27/2021 |
1.2.1 | 397 | 1/25/2021 |
1.2.0 | 376 | 1/25/2021 |
1.1.10 | 411 | 1/23/2021 |
1.1.9 | 434 | 1/23/2021 |
1.1.8 | 424 | 1/22/2021 |
1.1.7 | 404 | 1/22/2021 |
1.1.6 | 423 | 1/22/2021 |
1.1.5 | 414 | 1/22/2021 |
1.1.4 | 418 | 1/22/2021 |
1.1.3 | 454 | 1/22/2021 |
1.1.2 | 440 | 1/22/2021 |
1.1.1 | 449 | 1/21/2021 |
1.0.10 | 436 | 1/21/2021 |
1.0.9 | 451 | 11/9/2020 |
1.0.8 | 450 | 11/9/2020 |
1.0.7 | 479 | 11/9/2020 |
1.0.6 | 470 | 11/9/2020 |
1.0.5 | 451 | 11/9/2020 |
1.0.4 | 552 | 11/6/2020 |
1.0.3 | 405 | 11/6/2020 |
1.0.2 | 432 | 11/6/2020 |
1.0.1 | 480 | 11/6/2020 |
1.0.0 | 495 | 11/5/2020 |