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
<PackageReference Include="Neatoo.RemoteFactory.AspNetCore" Version="0.16.0" />
<PackageVersion Include="Neatoo.RemoteFactory.AspNetCore" Version="0.16.0" />
<PackageReference Include="Neatoo.RemoteFactory.AspNetCore" />
paket add Neatoo.RemoteFactory.AspNetCore --version 0.16.0
#r "nuget: Neatoo.RemoteFactory.AspNetCore, 0.16.0"
#:package Neatoo.RemoteFactory.AspNetCore@0.16.0
#addin nuget:?package=Neatoo.RemoteFactory.AspNetCore&version=0.16.0
#tool nuget:?package=Neatoo.RemoteFactory.AspNetCore&version=0.16.0
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
- What Problem Does RemoteFactory Solve? - The opportunity and approach
- Getting Started - Installation and first example
- Decision Guide - When to use what
- Factory Operations - Create, Fetch, Insert, Update, Delete, Execute, Event
- Service Injection - DI integration with
[Service]attribute - Authorization - Custom auth and ASP.NET Core policies
- Serialization - Ordinal vs Named formats
- Save Operation - IFactorySave routing pattern
- Factory Modes - Full, RemoteOnly, Logical
- Events - Fire-and-forget domain events
- ASP.NET Core Integration - Server-side configuration
- Attributes Reference - All available attributes
- Interfaces Reference - All available interfaces
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.
Links
| 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 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. |
-
net10.0
- Neatoo.RemoteFactory (>= 0.16.0)
-
net8.0
- Neatoo.RemoteFactory (>= 0.16.0)
-
net9.0
- Neatoo.RemoteFactory (>= 0.16.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
None yet