nostify 1.11.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package nostify --version 1.11.0
                    
NuGet\Install-Package nostify -Version 1.11.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="nostify" Version="1.11.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="nostify" Version="1.11.0" />
                    
Directory.Packages.props
<PackageReference Include="nostify" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add nostify --version 1.11.0
                    
#r "nuget: nostify, 1.11.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#addin nuget:?package=nostify&version=1.11.0
                    
Install nostify as a Cake Addin
#tool nuget:?package=nostify&version=1.11.0
                    
Install nostify as a Cake Tool

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:

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. image

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.

image

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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