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.
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.
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.
Domain model with factory methods:
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();
}
}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();
}
}Client code calls the factory:
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
}
}// 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);Server automatically exposes the endpoint at /api/neatoo. No controllers needed.
- 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)
Install NuGet packages:
Server project:
dotnet add package Neatoo.RemoteFactory
dotnet add package Neatoo.RemoteFactory.AspNetCoreClient project (Blazor WASM, etc.):
dotnet add package Neatoo.RemoteFactoryShared project (domain models):
dotnet add package Neatoo.RemoteFactoryConfigure client assembly for smaller output:
// In client assembly's AssemblyAttributes.cs:
// [assembly: FactoryMode(FactoryModeOption.RemoteOnly)]// In client assembly's AssemblyAttributes.cs:
// [assembly: FactoryMode(FactoryMode.RemoteOnly)]Server setup (ASP.NET Core):
// Server setup (Program.cs):
// services.AddNeatooAspNetCore(typeof(Person).Assembly);
// app.UseNeatoo();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>();
}
}Client setup (Blazor WASM):
// 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/") });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 });
}
}Domain model:
[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();
}[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");
}See Getting Started for a complete walkthrough.
- 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
- .NET 8.0 (LTS)
- .NET 9.0 (STS)
- .NET 10.0 (LTS)
Complete working examples in src/Examples/:
- Person - Simple Blazor WASM CRUD application
- OrderEntry - Order entry system with aggregate roots
MIT License - see LICENSE for details.