Neatoo.RemoteFactory.AspNetCore 0.16.0

dotnet add package Neatoo.RemoteFactory.AspNetCore --version 0.16.0
                    
NuGet\Install-Package Neatoo.RemoteFactory.AspNetCore -Version 0.16.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="Neatoo.RemoteFactory.AspNetCore" Version="0.16.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Neatoo.RemoteFactory.AspNetCore" Version="0.16.0" />
                    
Directory.Packages.props
<PackageReference Include="Neatoo.RemoteFactory.AspNetCore" />
                    
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 Neatoo.RemoteFactory.AspNetCore --version 0.16.0
                    
#r "nuget: Neatoo.RemoteFactory.AspNetCore, 0.16.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.
#:package Neatoo.RemoteFactory.AspNetCore@0.16.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Neatoo.RemoteFactory.AspNetCore&version=0.16.0
                    
Install as a Cake Addin
#tool nuget:?package=Neatoo.RemoteFactory.AspNetCore&version=0.16.0
                    
Install as a Cake Tool

Neatoo RemoteFactory

Roslyn Source Generator-powered Data Mapper Factory for 3-tier .NET applications

RemoteFactory eliminates DTOs, manual factories, and API controllers by generating everything at compile time. Write domain model methods once, get client and server implementations automatically.

The Opportunity

With Blazor WebAssembly, the same .NET library can run on both client and server. This changes everything.

The old world (JavaScript SPA + ASP.NET Core): Your domain model lived on the server. To get data to the browser, you serialized to JSON, wrote DTOs to receive it, and mapped back and forth. Two representations of the same thing.

The new world (Blazor WASM + ASP.NET Core): Your domain model can run in the browser. The same Employee class executes client-side and server-side. No translation layer needed.

RemoteFactory makes this practical. It generates the factories, serialization, and HTTP plumbing so your domain methods "just work" across the wire. One source of truth, zero boilerplate.

Why RemoteFactory?

Traditional 3-tier architecture:

  • Write domain model methods
  • Create DTOs to transfer state
  • Build factories to map between DTOs and domain models
  • Write API controllers to expose operations
  • Maintain all four layers as requirements change

With RemoteFactory:

  • Write domain model methods
  • Add attributes ([Factory], [Remote], [Create], [Fetch], [Insert], [Update], [Delete], etc.)
  • Done. Generator creates factories, serialization, and endpoints.

Quick Example

Domain model with factory methods:

<a id='snippet-readme-domain-model'></a>

public interface IPerson : IFactorySaveMeta
{
    Guid Id { get; }
    string FirstName { get; set; }
    string LastName { get; set; }
    string? Email { get; set; }
    new bool IsDeleted { get; set; }
}

[Factory]
[SuppressFactory] // Suppress factory generation for documentation sample
public partial class Person : IPerson
{
    public Guid Id { get; private set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string? Email { get; set; }
    public bool IsNew { get; private set; } = true;
    public bool IsDeleted { get; set; }

    [Create]
    public Person()
    {
        Id = Guid.NewGuid();
    }

    [Remote]
    [Fetch]
    public async Task<bool> Fetch(Guid id, [Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(id);
        if (entity == null) return false;

        Id = entity.Id;
        FirstName = entity.FirstName;
        LastName = entity.LastName;
        Email = entity.Email;
        IsNew = false;
        return true;
    }

    [Remote]
    [Insert]
    public async Task Insert([Service] IPersonRepository repository)
    {
        var entity = new PersonEntity
        {
            Id = Id,
            FirstName = FirstName,
            LastName = LastName,
            Email = Email,
            Created = DateTime.UtcNow,
            Modified = DateTime.UtcNow
        };
        await repository.AddAsync(entity);
        await repository.SaveChangesAsync();
        IsNew = false;
    }

    [Remote]
    [Update]
    public async Task Update([Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(Id)
            ?? throw new InvalidOperationException($"Person {Id} not found");

        entity.FirstName = FirstName;
        entity.LastName = LastName;
        entity.Email = Email;
        entity.Modified = DateTime.UtcNow;

        await repository.UpdateAsync(entity);
        await repository.SaveChangesAsync();
    }

    [Remote]
    [Delete]
    public async Task Delete([Service] IPersonRepository repository)
    {
        await repository.DeleteAsync(Id);
        await repository.SaveChangesAsync();
    }
}

<sup><a href='/src/docs/reference-app/EmployeeManagement.Domain/Samples/ReadmeSamples.cs#L9-L93' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-domain-model' title='Start of snippet'>anchor</a></sup> <a id='snippet-readme-domain-model-1'></a>

public interface IPerson : IFactorySaveMeta
{
    Guid Id { get; }
    string FirstName { get; set; }
    string LastName { get; set; }
    string? Email { get; set; }
    new bool IsDeleted { get; set; }
}

[Factory]
public partial class Person : IPerson
{
    public Guid Id { get; private set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string? Email { get; set; }
    public bool IsNew { get; private set; } = true;
    public bool IsDeleted { get; set; }

    [Create]
    public Person()
    {
        Id = Guid.NewGuid();
    }

    [Remote]
    [Fetch]
    public async Task<bool> Fetch(Guid id, [Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(id);
        if (entity == null) return false;

        Id = entity.Id;
        FirstName = entity.FirstName;
        LastName = entity.LastName;
        Email = entity.Email;
        IsNew = false;
        return true;
    }

    [Remote]
    [Insert]
    public async Task Insert([Service] IPersonRepository repository)
    {
        var entity = new PersonEntity
        {
            Id = Id,
            FirstName = FirstName,
            LastName = LastName,
            Email = Email,
            Created = DateTime.UtcNow,
            Modified = DateTime.UtcNow
        };
        await repository.AddAsync(entity);
        await repository.SaveChangesAsync();
        IsNew = false;
    }

    [Remote]
    [Update]
    public async Task Update([Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(Id)
            ?? throw new InvalidOperationException($"Person {Id} not found");

        entity.FirstName = FirstName;
        entity.LastName = LastName;
        entity.Email = Email;
        entity.Modified = DateTime.UtcNow;

        await repository.UpdateAsync(entity);
        await repository.SaveChangesAsync();
    }

    [Remote]
    [Delete]
    public async Task Delete([Service] IPersonRepository repository)
    {
        await repository.DeleteAsync(Id);
        await repository.SaveChangesAsync();
    }
}

<sup><a href='/src/docs/samples/ReadmeSamples.cs#L13-L96' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-domain-model-1' title='Start of snippet'>anchor</a></sup>

Client code calls the factory:

<a id='snippet-readme-client-usage'></a>

public static class ClientUsageExample
{
    // IPersonFactory is auto-generated from Person class
    public static async Task BasicOperations(IPersonFactory factory)
    {
        // Create a new person
        var person = factory.Create();
        person.FirstName = "John";
        person.LastName = "Doe";
        person.Email = "john.doe@example.com";

        // Save (routes to Insert because IsNew = true)
        await factory.Save(person);

        // Fetch existing
        var existing = await factory.Fetch(person.Id);

        // Update
        existing!.Email = "john.updated@example.com";
        await factory.Save(existing);  // Routes to Update

        // Delete
        existing.IsDeleted = true;
        await factory.Save(existing);  // Routes to Delete
    }
}

<sup><a href='/src/docs/reference-app/EmployeeManagement.Domain/Samples/ReadmeSamples.cs#L95-L122' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-client-usage' title='Start of snippet'>anchor</a></sup> <a id='snippet-readme-client-usage-1'></a>

// Create a new person
// var person = factory.Create();
// person.FirstName = "John";
// person.LastName = "Doe";
// person.Email = "john.doe@example.com";
//
// // Save routes to Insert (IsNew = true)
// var saved = await factory.Save(person);
//
// // Fetch an existing person
// var fetched = await factory.Fetch(saved!.Id);
// // fetched.FirstName is "John"
//
// // Update - Save routes to Update (IsNew = false)
// fetched!.Email = "john.updated@example.com";
// await factory.Save(fetched);
//
// // Delete - set IsDeleted, then Save
// fetched.IsDeleted = true;
// await factory.Save(fetched);

<sup><a href='/src/docs/samples/ReadmeSamples.cs#L98-L119' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-client-usage-1' title='Start of snippet'>anchor</a></sup>

Server automatically exposes the endpoint at /api/neatoo. No controllers needed.

Key Features

  • Zero boilerplate: No DTOs, no manual mapping, no controllers
  • Type-safe: Roslyn generates strongly-typed factories from your domain methods
  • DI integration: Inject services into factory methods with [Service] attribute
  • Authorization: Built-in support for custom auth or ASP.NET Core policies
  • Compact serialization: Ordinal format reduces payloads by 40-50%
  • Lifecycle hooks: IFactoryOnStart, IFactoryOnComplete, IFactoryOnCancelled
  • Fire-and-forget events: Domain events with scope isolation via [Event] attribute
  • Flexible modes: Full (server), RemoteOnly (client), or Logical (single-tier)

Installation

Install NuGet packages:

Server project:

dotnet add package Neatoo.RemoteFactory
dotnet add package Neatoo.RemoteFactory.AspNetCore

Client project (Blazor WASM, etc.):

dotnet add package Neatoo.RemoteFactory

Shared project (domain models):

dotnet add package Neatoo.RemoteFactory

Configure client assembly for smaller output:

<a id='snippet-readme-client-assembly-mode'></a>

// In client assembly's AssemblyAttributes.cs:
// [assembly: FactoryMode(FactoryModeOption.RemoteOnly)]

<sup><a href='/src/docs/reference-app/EmployeeManagement.Domain/Samples/ReadmeSamples.cs#L124-L127' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-client-assembly-mode' title='Start of snippet'>anchor</a></sup> <a id='snippet-readme-client-assembly-mode-1'></a>

// In client assembly's AssemblyAttributes.cs:
// [assembly: FactoryMode(FactoryMode.RemoteOnly)]

<sup><a href='/src/docs/samples/ReadmeSamples.cs#L153-L156' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-client-assembly-mode-1' title='Start of snippet'>anchor</a></sup>

Getting Started

Server setup (ASP.NET Core):

<a id='snippet-readme-server-setup'></a>

// Server setup (Program.cs):
// services.AddNeatooAspNetCore(typeof(Person).Assembly);
// app.UseNeatoo();

<sup><a href='/src/docs/reference-app/EmployeeManagement.Domain/Samples/ReadmeSamples.cs#L129-L133' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-server-setup' title='Start of snippet'>anchor</a></sup> <a id='snippet-readme-server-setup-1'></a>

public static class ServerSetup
{
    public static void ConfigureServices(IServiceCollection services)
    {
        // Register Neatoo ASP.NET Core services
        // services.AddNeatooAspNetCore(typeof(Person).Assembly);

        // Register domain services
        // services.AddScoped<IPersonRepository, PersonRepository>();
    }
}

<sup><a href='/src/docs/samples/ReadmeSamples.cs#L121-L133' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-server-setup-1' title='Start of snippet'>anchor</a></sup>

Client setup (Blazor WASM):

<a id='snippet-readme-client-setup'></a>

// Client setup (Program.cs):
// services.AddNeatooRemoteFactory(
//     NeatooFactory.Remote,
//     new NeatooSerializationOptions { Format = SerializationFormat.Ordinal },
//     typeof(Person).Assembly);
// services.AddKeyedScoped(RemoteFactoryServices.HttpClientKey, (sp, key) =>
//     new HttpClient { BaseAddress = new Uri("https://api.example.com/") });

<sup><a href='/src/docs/reference-app/EmployeeManagement.Domain/Samples/ReadmeSamples.cs#L135-L143' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-client-setup' title='Start of snippet'>anchor</a></sup> <a id='snippet-readme-client-setup-1'></a>

public static class ClientSetup
{
    public static void ConfigureServices(IServiceCollection services, Uri baseAddress)
    {
        // Register Neatoo RemoteFactory for client
        // services.AddNeatooRemoteFactory(
        //     NeatooFactory.Remote,
        //     typeof(Person).Assembly);

        // Register keyed HttpClient for Neatoo
        // services.AddKeyedScoped(
        //     RemoteFactoryServices.HttpClientKey,
        //     (sp, key) => new HttpClient { BaseAddress = baseAddress });
    }
}

<sup><a href='/src/docs/samples/ReadmeSamples.cs#L135-L151' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-client-setup-1' title='Start of snippet'>anchor</a></sup>

Domain model:

<a id='snippet-readme-full-example'></a>

[Factory]
[SuppressFactory] // Suppress factory generation for documentation sample
[AuthorizeFactory<IPersonAuthorization>]
public partial class PersonWithAuth : IFactorySaveMeta
{
    public Guid Id { get; private set; }
    public string FirstName { get; set; } = "";
    public string LastName { get; set; } = "";
    public bool IsNew { get; private set; } = true;
    public bool IsDeleted { get; set; }

    [Create]
    public PersonWithAuth() { Id = Guid.NewGuid(); }

    [Remote, Fetch]
    public async Task<bool> Fetch(
        Guid id,
        [Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(id);
        if (entity == null) return false;
        Id = entity.Id;
        FirstName = entity.FirstName;
        LastName = entity.LastName;
        IsNew = false;
        return true;
    }

    [Remote, Insert]
    public async Task Insert([Service] IPersonRepository repository)
    {
        await repository.AddAsync(new PersonEntity
        {
            Id = Id, FirstName = FirstName, LastName = LastName,
            Email = "", Created = DateTime.UtcNow, Modified = DateTime.UtcNow
        });
        await repository.SaveChangesAsync();
        IsNew = false;
    }

    [Remote, Update]
    public async Task Update([Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(Id)
            ?? throw new InvalidOperationException();
        entity.FirstName = FirstName;
        entity.LastName = LastName;
        entity.Modified = DateTime.UtcNow;
        await repository.UpdateAsync(entity);
        await repository.SaveChangesAsync();
    }

    [Remote, Delete]
    public async Task Delete([Service] IPersonRepository repository)
    {
        await repository.DeleteAsync(Id);
        await repository.SaveChangesAsync();
    }
}

public interface IPersonAuthorization
{
    [AuthorizeFactory(AuthorizeFactoryOperation.Read)]
    bool CanRead();

    [AuthorizeFactory(AuthorizeFactoryOperation.Write)]
    bool CanWrite();
}

<sup><a href='/src/docs/reference-app/EmployeeManagement.Domain/Samples/ReadmeSamples.cs#L145-L214' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-full-example' title='Start of snippet'>anchor</a></sup> <a id='snippet-readme-full-example-1'></a>

[Factory]
[AuthorizeFactory<IPersonAuthorization>]
public partial class PersonWithAuth : IFactorySaveMeta
{
    public Guid Id { get; private set; }
    public string FirstName { get; set; } = string.Empty;
    public string LastName { get; set; } = string.Empty;
    public string? Email { get; set; }
    public bool IsNew { get; private set; } = true;
    public bool IsDeleted { get; set; }

    [Create]
    public PersonWithAuth()
    {
        Id = Guid.NewGuid();
    }

    [Remote]
    [Fetch]
    public async Task<bool> Fetch(Guid id, [Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(id);
        if (entity == null) return false;

        Id = entity.Id;
        FirstName = entity.FirstName;
        LastName = entity.LastName;
        Email = entity.Email;
        IsNew = false;
        return true;
    }

    [Remote]
    [Insert]
    public async Task Insert([Service] IPersonRepository repository)
    {
        var entity = new PersonEntity
        {
            Id = Id,
            FirstName = FirstName,
            LastName = LastName,
            Email = Email,
            Created = DateTime.UtcNow,
            Modified = DateTime.UtcNow
        };
        await repository.AddAsync(entity);
        await repository.SaveChangesAsync();
        IsNew = false;
    }

    [Remote]
    [Update]
    public async Task Update([Service] IPersonRepository repository)
    {
        var entity = await repository.GetByIdAsync(Id)
            ?? throw new InvalidOperationException($"Person {Id} not found");

        entity.FirstName = FirstName;
        entity.LastName = LastName;
        entity.Email = Email;
        entity.Modified = DateTime.UtcNow;

        await repository.UpdateAsync(entity);
        await repository.SaveChangesAsync();
    }

    [Remote]
    [Delete]
    public async Task Delete([Service] IPersonRepository repository)
    {
        await repository.DeleteAsync(Id);
        await repository.SaveChangesAsync();
    }
}

public interface IPersonAuthorization
{
    [AuthorizeFactory(AuthorizeFactoryOperation.Create)]
    bool CanCreate();

    [AuthorizeFactory(AuthorizeFactoryOperation.Read)]
    bool CanRead();

    [AuthorizeFactory(AuthorizeFactoryOperation.Write)]
    bool CanWrite();
}

public class PersonAuthorization : IPersonAuthorization
{
    private readonly IUserContext _userContext;

    public PersonAuthorization(IUserContext userContext)
    {
        _userContext = userContext;
    }

    public bool CanCreate() => _userContext.IsAuthenticated;
    public bool CanRead() => _userContext.IsAuthenticated;
    public bool CanWrite() =>
        _userContext.IsInRole("Admin") || _userContext.IsInRole("Manager");
}

<sup><a href='/src/docs/samples/ReadmeSamples.cs#L158-L260' title='Snippet source file'>snippet source</a> | <a href='#snippet-readme-full-example-1' title='Start of snippet'>anchor</a></sup>

See Getting Started for a complete walkthrough.

Documentation

Supported Frameworks

  • .NET 8.0 (LTS)
  • .NET 9.0 (STS)
  • .NET 10.0 (LTS)

Examples

Complete working examples in src/Examples/:

  • Person - Simple Blazor WASM CRUD application
  • OrderEntry - Order entry system with aggregate roots

License

MIT License - see LICENSE for details.

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 is compatible.  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 is compatible.  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. 
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
0.16.0 0 3/1/2026
0.15.0 35 2/27/2026
Loading failed

None yet