Every time a client asked for a new field, a new section, or a slightly different layout, the process was the same. Update the model. Update the UI. Maybe add a column to the database. Push a deployment. Review, staging, production. For something that should take five minutes.
If you work on a multi-tenant SaaS it gets worse. Different tenants want different fields and different layouts. You either build a rigid config system that never quite covers the next request, or you end up with a bloated form component that tries to handle every case at once.
I looked for something off the shelf. Most options were JS-heavy and didn't sit cleanly inside a Blazor app. Others needed a separate hosted backend. None of them were just a self-contained .NET package I could drop in and own.
So I built one.
The idea
The goal was simple. Let the right users (admins, or whoever has the rights) design their own forms directly inside the app. No code changes, no deployment.
The part I cared about most: it had to stay type-safe by default. You start from a plain C# model, your own class, with your own data annotations. Formaze generates the form from it, and admins arrange the layout, rename labels, group fields and reorder them live. Fields that map to your model bind straight back to your C# properties, with data-annotation validation. No opaque JSON blob you have to map by hand on both ends.
And when an admin needs a field that isn't on your model at all, they can add a dynamic one. These are kept as typed key/value entries on the side, so the editor never forces you to round-trip through the model first.
The whole thing lives inside the .NET ecosystem. Nothing to host, nothing to call out to.
How it works
Install the package:
dotnet add package Formaze.Blazor.MudBlazor
Register the services (Formaze builds on MudBlazor, so that has to be registered too):
// Program.cs
builder.Services.AddMudServices();
builder.Services.AddFormazeJson(); // JSON store; in-memory, EF Core and custom stores also exist
Define a model like you normally would:
public class ContactModel
{
[Required]
[Display(Name = "Full name", GroupName = "Identity")]
public string? Name { get; set; }
[EmailAddress]
[Display(Name = "Email", GroupName = "Contact")]
public string? Email { get; set; }
[DataType(DataType.MultilineText)]
[Display(Name = "Message", GroupName = "Contact")]
public string? Message { get; set; }
}
Then drop a single component on the page. The same component handles both sides: flip EditMode on for the people allowed to design the form, off for the people filling it in:
<FormazeComponent T="ContactModel"
Key="contact"
Model="_model"
EditMode="_isAdmin"
OnValidSubmit="HandleSubmit" />
The form an admin builds visually, with fields grouped into labeled sections and dragged into order, is exactly the layout users get. The configuration is saved against the Key through a store (IFormazeStore), so you decide where it lives: a JSON file, your EF Core database, or your own implementation. No mapping layer in between, and no redeploy to change a form.
The editor, live. Try it in the browser.
What you can actually do
Beyond the basic generate-a-form story, the features that turned out to matter most in practice:
- Conditional fields: show a field only when another one equals a given value, configured live in the editor (works for dynamic fields too).
- Dynamic fields: let an admin add a field that doesn't exist on your model, no code change.
- Model sync: add a property to your C# class and it shows up automatically in a "New fields" group on the next render. Nothing to wire up.
- Custom field renderers: override how any single field renders with your own component, with automatic fallback to the default.
-
Pluggable storage: JSON, in-memory, EF Core, or your own
IFormazeStore. - Per-field configuration: labels, placeholders, helper text, required/disabled/read-only, min/max/step, masks, date formats, and 1 to 4 column layouts.
Conditional fields are the one I'd show first. An admin marks a field "show only when...", picks the controlling field and the value, and it's live, with no recompile:
// On the model, the field is just a normal property:
[Display(Name = "Other (please specify)", GroupName = "Feedback")]
public string? OtherReason { get; set; }
// In the editor, the admin sets: show "OtherReason" only when "Reason" == "Other".
// Hidden fields aren't rendered *or* validated.
What it does not do (yet)
Being honest here, because it matters when you decide whether to adopt something.
It depends on MudBlazor, and that's a real consideration today. You can already swap the rendered fields for your own components through the CustomFieldRenderer parameter (anything you don't override falls back to the default renderer), so your end users don't have to get a Mud-flavored UI. What isn't decoupled yet is the dependency itself and the admin editor, which is built on MudBlazor. So Mud lands in your dependency graph regardless, and making the whole thing component-agnostic is on the roadmap.
It needs Interactive Server or WebAssembly render mode. Static SSR is not supported.
Keyboard navigation for the drag-and-drop was the main accessibility gap at launch. That came straight out of community feedback on the first release, and it's the kind of thing I'd want to know about before installing anything. It's been reworked since: keyboard reordering, screen-reader field groups and visible focus now ship as part of a WCAG 2.2 AA pass, and the remaining edges are tracked.
Why I wrote this up
I shipped the first version, posted it, and got a pile of genuinely useful feedback (bugs, accessibility, edge cases). Several releases later, a lot of that feedback is now turned into fixes. Writing the experience down felt more useful than another "look at my thing" post, because the underlying problem (forms that change faster than your release cycle) is one a lot of Blazor devs hit.
If it sounds like something you've fought with, the package is on NuGet and there's a live demo at formaze.dev/demo. It's free to use (with a small "Powered by Formaze" watermark). Feedback welcome, especially the critical kind.

Top comments (0)