Sphere10.Framework.Web.AspNetCore.MVC
3.2.0-ci.163.43664782937.1
dotnet add package Sphere10.Framework.Web.AspNetCore.MVC --version 3.2.0-ci.163.43664782937.1
NuGet\Install-Package Sphere10.Framework.Web.AspNetCore.MVC -Version 3.2.0-ci.163.43664782937.1
<PackageReference Include="Sphere10.Framework.Web.AspNetCore.MVC" Version="3.2.0-ci.163.43664782937.1" />
<PackageVersion Include="Sphere10.Framework.Web.AspNetCore.MVC" Version="3.2.0-ci.163.43664782937.1" />
<PackageReference Include="Sphere10.Framework.Web.AspNetCore.MVC" />
paket add Sphere10.Framework.Web.AspNetCore.MVC --version 3.2.0-ci.163.43664782937.1
#r "nuget: Sphere10.Framework.Web.AspNetCore.MVC, 3.2.0-ci.163.43664782937.1"
#:package Sphere10.Framework.Web.AspNetCore.MVC@3.2.0-ci.163.43664782937.1
#addin nuget:?package=Sphere10.Framework.Web.AspNetCore.MVC&version=3.2.0-ci.163.43664782937.1&prerelease
#tool nuget:?package=Sphere10.Framework.Web.AspNetCore.MVC&version=3.2.0-ci.163.43664782937.1&prerelease
Sphere10.Framework.Web.AspNetCore.MVC
This library extends ASP.NET Core MVC applications that use Sphere10 Framework with action results, Razor HTML helpers, AJAX Bootstrap forms and action filters. It builds on the shared ASP.NET Core integration and does not register controllers, routes or client scripts by itself.
Features
| Area | APIs |
|---|---|
| Action results | XmlResult for serialized objects or existing XML, and the Windows-only ImageResult for System.Drawing images. |
| Views | Bootstrap HTML helpers, enum select lists and Controller.RenderViewAsync. |
| AJAX forms | FormModelBase, BootstrapFormScope<TModel>, FormScopeOptions, FormResult and the embedded forms script. |
| Validation and filters | ModelState extensions, CleanupAttribute, FilterFactoryBase<TFilter> and FormActionAttribute. |
| Routing and URLs | UrlExtensions.Content and CustomRouteAttribute. |
Installation
Reference the package from an ASP.NET Core project that targets .NET 10 or later:
dotnet add package Sphere10.Framework.Web.AspNetCore.MVC
Pass --version to match the other Sphere10.Framework packages in the application, and --source to restore from a local folder feed. The root README explains how to build and publish the packages, including to a private folder feed.
To build against the source instead, reference the project. From the repository root, these commands create the host for the examples below, beside the repository:
dotnet new web --name MvcDemo --output ../MvcDemo --framework net10.0
dotnet add ../MvcDemo/MvcDemo.csproj reference src/Sphere10.Framework.Web.AspNetCore.MVC/Sphere10.Framework.Web.AspNetCore.MVC.csproj
Quick start: serve a sitemap
Replace MvcDemo/Program.cs with:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
app.UseStaticFiles();
app.MapControllers();
app.Run();
Add Controllers/SitemapController.cs:
using Microsoft.AspNetCore.Mvc;
using Sphere10.Framework.Web;
using Sphere10.Framework.Web.AspNetCore.MVC;
namespace MvcDemo.Controllers;
public class SitemapController : Controller {
[HttpGet("sitemap.xml")]
public IActionResult Index() {
var sitemap = new SitemapXml();
sitemap.Add("https://example.com/", frequency: SitemapXml.Frequency.Weekly, priority: 1.0);
return new XmlResult(sitemap);
}
}
Run the host from the repository root, then open /sitemap.xml at the address that it prints:
dotnet run --project ../MvcDemo -p:BuildRevision=0
XmlResult is a ContentResult with the application/xml content type and status 200 unless you pass another status code:
| Constructor | Response body |
|---|---|
XmlResult(object), XmlResult(object, statusCode) |
The object serialized by Tools.Xml.WriteToString, with the namespace of its [XmlRoot] as the default namespace. |
XmlResult(string), XmlResult(string, statusCode) |
The string as given, so a string argument is never serialized. |
XmlResult(), XmlResult(statusCode) |
Empty. |
XML results do not need the Framework to be started. Add the registration and start calls from the shared hosting quick start when the application needs Framework modules and lifecycle services.
Domains
Every folder of Sphere10.Framework.Web.AspNetCore.MVC is a domain with its own README, which lists all of its public types.
| Domain | What it provides | Source |
|---|---|---|
| Extensions | Extension methods for MVC controllers, Razor HTML helpers, URL helpers, select lists and model state. | source |
| Filters | MVC action filter attributes for per-action cleanup, service-resolved filters and form-tag flags. | source |
| Forms | Bootstrap AJAX form models, rendering scope, options and JSON results for the embedded forms script. | source |
| Processing | A Windows-only action result that writes a System.Drawing image to the response. |
source |
| Routing | A controller attribute that declares conventional routes and registers them from an assembly. | source |
| XML | An action result that returns serialized objects or existing XML as application/xml. |
source |
Project root
Types declared in the project folder itself:
| Type | Kind | Purpose |
|---|---|---|
Tools.Web.Mvc |
tools | Builds an MVC SelectList from an enum type, labelled by each member's description, with an optional selected value and sort direction. |
Select lists
using System;
using Sphere10.Framework;
var weekdays = Tools.Web.Mvc.ToSelectList<DayOfWeek>(DayOfWeek.Monday, SortDirection.Ascending);
Pass the returned SelectList to Html.DropDownListFor or Html.BootstrapDropDownListFor. Each option's value is the enum member's name, and its label comes from Tools.Enums.GetDescription, which reads a [Description] attribute and falls back to the name. The optional sort orders the labels and keeps the selected value. ToSelectList(Type, ...) takes the enum type at run time instead.
For other collections, the ToSelectList(text, value, defaultOption, selected) extension on IEnumerable<T> builds SelectListItem objects from delegates. A defaultOption becomes the first item, with the value -1.
Bootstrap AJAX forms
BootstrapFormScope<TModel> renders a form that the embedded hydrogen-bootstrap-forms-1.0.0.js script submits with jQuery AJAX. The action answers with a FormResult, and the script then shows a message, redirects, or replaces the page or the form. The steps below add a contact form to the sitemap host.
Prerequisites
The application supplies the client libraries, because this package neither downloads nor serves them:
- jQuery, loaded before Bootstrap. The script calls Bootstrap's jQuery
tooltipandpopoverplugins, which Bootstrap registers only when jQuery is present. - Bootstrap 5, whose
data-bs-*attributes andform-control,form-selectandbtn-closeclasses the helpers and the script use. - Choices.js for any drop-down that
BootstrapDropDownListForrenders with its defaultuseChoicesJS: true. Without the library, the form's initialization stops at that element and the form is not converted to AJAX, so passuseChoicesJS: falsefor a plain select element. - Font Awesome, optionally, for the
faicons in result alerts.
The example's view loads them from wwwroot/lib/jquery/jquery.min.js, wwwroot/lib/bootstrap/bootstrap.bundle.min.js and wwwroot/lib/bootstrap/bootstrap.min.css.
Serve the embedded script
The script is an embedded resource of the MVC assembly, not a static web asset. Add these imports at the top of Program.cs and the endpoint before app.Run():
using Microsoft.AspNetCore.Http;
using Sphere10.Framework;
using Sphere10.Framework.Web.AspNetCore.MVC;
app.MapGet("/sphere10/forms.js", () => {
var script = typeof(FormModelBase).Assembly.GetManifestResourceStream(
"Sphere10.Framework.Web.AspNetCore.MVC.Forms.hydrogen-bootstrap-forms-1.0.0.js"
);
Guard.Ensure(script != null, "The embedded forms script was not found.");
return Results.Stream(script, "text/javascript");
});
Results.Stream disposes the resource stream once the response is written. Load the script in the page head, without defer or async, because each form calls the script's F_Init function from an inline script.
Define the model and controller
Save Models/ContactForm.cs:
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using Sphere10.Framework.Web.AspNetCore.MVC;
namespace MvcDemo.Models;
public sealed class ContactForm : FormModelBase {
[Required]
[DisplayName("Your name")]
public string Name { get; set; } = string.Empty;
}
FormModelBase gives each model a random, DOM-safe ID, which becomes the form's element ID, and a SubmitCount that the script increments on every submission.
Save Controllers/ContactController.cs:
using Microsoft.AspNetCore.Mvc;
using MvcDemo.Models;
using Newtonsoft.Json;
using Sphere10.Framework.Web.AspNetCore.MVC;
namespace MvcDemo.Controllers;
[Route("contact")]
public class ContactController : Controller {
[HttpGet]
public IActionResult Index() => View(new ContactForm());
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Submit(ContactForm model) {
var result = new FormResult {
Result = ModelState.IsValid,
ResultType = FormResultType.ShowMessage,
Message = ModelState.IsValid ? "Demo submission accepted. No data was stored." : "Enter your name."
};
return Content(JsonConvert.SerializeObject(result), "application/json");
}
}
FormResult carries Newtonsoft.Json attributes, so the action serializes it with JsonConvert to produce the result, message, type, url and content fields that the script reads. ASP.NET Core's default System.Text.Json output would name the type field resultType and write it as a number, so returning Json(result) does not work. No MVC Newtonsoft integration package is needed, because this library already references Newtonsoft.Json.
Render the form
Save Views/Contact/Index.cshtml:
@using Microsoft.AspNetCore.Mvc.Rendering
@using Sphere10.Framework.Web.AspNetCore.MVC
@model MvcDemo.Models.ContactForm
@{
Layout = null;
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Contact form demo</title>
<link rel="stylesheet" href="/lib/bootstrap/bootstrap.min.css" />
<script src="/lib/jquery/jquery.min.js"></script>
<script src="/lib/bootstrap/bootstrap.bundle.min.js"></script>
<script src="/sphere10/forms.js"></script>
</head>
<body>
<main class="container py-4">
@using (Html.BeginBootstrapForm("/contact", Model, options: FormScopeOptions.UseLoadingOverlay | FormScopeOptions.AddAntiForgeryToken)) {
@Html.BootstrapLabelFor(model => model.Name)
@Html.BootstrapTextBoxFor(model => model.Name)
@Html.BootstrapFormButton(Model, "Submit")
}
</main>
</body>
</html>
Stop the host, run it again and open /contact. Submitting the form posts it with AJAX and shows the action's message at the end of the form. The field keeps its value after submission, because the options omit ClearOnSuccess.
The scope writes the <form> tag with the model's ID, the hidden ID and SubmitCount fields, the antiforgery token and, with UseLoadingOverlay, a loading overlay. Disposing the scope writes a {formId}_message_marker element, the F_Init call and the closing tag.
The embedded script shows each result in an alert after that marker, and later results replace the alert. A page can instead supply an element whose ID is {formId}_result, which the script replaces with the alert. Call MessageMarker() to place the scope's marker earlier in the form, and the scope then omits it on disposal.
BootstrapFormScope implements both Dispose and DisposeAsync. DisposeAsync writes the closing markup with asynchronous writes only.
Results and options
FormResultType |
type value |
Script behavior |
|---|---|---|
ShowMessage |
message |
Shows message in a success alert when result is true and in a danger alert otherwise. |
Redirect |
redirect |
Navigates to url. |
ReplacePage |
replace_page |
Replaces the whole document with content. |
ReplaceForm |
replace_form |
Replaces the form with content, then shows message when it is set. |
FormScopeOptions flag |
Effect |
|---|---|
UseLoadingOverlay |
Renders an overlay inside the form tag, which the script shows during each submission to block input. Without the flag, the scope renders no overlay and the script shows none. |
ClearOnSuccess |
Restores the form's original markup after a successful result. |
OmitFormTag |
Writes only the marker and the F_Init call, so the view supplies a <form> element whose ID is the model's ID. |
AddAntiForgeryToken |
Writes an antiforgery token inside the form. |
BotProtect |
Base64-encodes the action URL, which the script decodes before posting. It requires the URL overload of BeginBootstrapForm, because the action and controller overload throws with this flag. |
UseGet |
Declares get on the form tag, and the script sends the fields in the query string of a GET request instead of posting them. The action must accept GET. |
SmallerBlockerOverlay |
Sizes the overlay at 90% of the form instead of 100%. |
Default |
UseLoadingOverlay, AddAntiForgeryToken, BotProtect and ClearOnSuccess. |
DefaultRetainInput |
Default without ClearOnSuccess. |
BotProtect deters generic form bots only. It does not replace server-side validation or the antiforgery check.
Bootstrap input helpers
The Bootstrap*For helpers wrap the MVC *For helpers with Bootstrap 5 markup. Each takes its label or placeholder text from the member's [DisplayName] attribute and appends * for a [Required] member. When ModelState holds errors for the member, the input gets the is-invalid class and a Bootstrap tooltip that lists the errors.
| Helper | Renders |
|---|---|
BootstrapLabelFor |
A label. |
BootstrapTextBoxFor, BootstrapPasswordFor |
A form-control input. The text box can be read-only. |
BootstrapTextAreaFor |
A form-control text area whose height follows approxRows. |
BootstrapDropDownListFor |
A form-select element with the display name as its empty option, enhanced by Choices.js unless useChoicesJS is false. |
BootstrapCheckBoxAndLabelFor |
A form-check check box and its label. |
BootstrapFormButton |
The submit button of a form scope, with a spinner that the script shows during submission. |
ValidationMessageForEx |
A validation message styled as a Bootstrap warning badge, with the badge bg-warning text-dark classes, optionally floated to the end. |
Render a view to a string
Controller.RenderViewAsync(viewName, model, partial) renders an MVC view into a string, for example as the content of a ReplaceForm result. An empty viewName selects the current action's view, and partial: true renders it as a partial view. When the view engine finds no view, the method returns the text A view with the name <viewName> could not be found instead of throwing, so check for a missing view before you send the result.
To replace the contact form with a thank-you fragment, save Views/Contact/Thanks.cshtml:
@model MvcDemo.Models.ContactForm
<div class="alert alert-success">Thank you, @Model.Name.</div>
Then change the Submit action of ContactController to the version below, and add using System.Threading.Tasks; and using Sphere10.Framework; to the file:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Submit(ContactForm model) {
var nameCheck = model.Name == "admin" ? Result.Error("Choose another name.") : Result.Success;
ModelState.AddModelError(nameof(ContactForm.Name), nameCheck);
var result = ModelState.IsValid
? new FormResult {
Result = true,
ResultType = FormResultType.ReplaceForm,
Content = await this.RenderViewAsync("Thanks", model, partial: true)
}
: new FormResult {
Result = false,
ResultType = FormResultType.ShowMessage,
Message = "Correct the highlighted fields."
};
return Content(JsonConvert.SerializeObject(result), "application/json");
}
RenderViewAsync is an extension method, so a controller calls it through this. AddModelError(key, Result) adds each error message of a Sphere10.Framework.Result under the key, and a successful Result adds nothing. The other overloads add a Result or a formatted message under the empty key, and Remove(params string[]) removes several keys at once.
Images
ImageResult writes a System.Drawing.Image to the response as JPEG, which is the default, or as PNG or GIF, and throws for any other ImageFormat. It is marked Windows-only and disposes the image after writing, so do not reuse the image. MVC executes it asynchronously: it encodes the image in memory, sets the content length and copies the bytes to the response body with asynchronous writes, so AllowSynchronousIO is not needed. Only a direct call to the synchronous ExecuteResult saves straight to the response body.
This controller draws a small PNG badge on each request:
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Versioning;
using Microsoft.AspNetCore.Mvc;
using Sphere10.Framework.Web.AspNetCore.MVC;
namespace MvcDemo.Controllers;
[SupportedOSPlatform("windows")]
public class BadgeController : Controller {
[HttpGet("badge.png")]
public IActionResult Badge() {
var bitmap = new Bitmap(120, 24);
using (var graphics = Graphics.FromImage(bitmap)) {
graphics.Clear(Color.SteelBlue);
graphics.DrawString("Sphere10", SystemFonts.DefaultFont, Brushes.White, 4, 4);
}
return new ImageResult(bitmap, ImageFormat.Png);
}
}
The result disposes the bitmap after writing it, so the action does not dispose it. The one-argument constructor writes JPEG.
Filters, routing and URLs
| API | Behavior |
|---|---|
CleanupAttribute |
Gives the action a ViewBag.Cleanup list of Action delegates and runs them after the result executes. A failing delegate is logged through SystemLog, and the next one still runs. |
FilterFactoryBase<TFilter> |
Base attribute that resolves TFilter from the request services on every request. Register TFilter in the service collection, because an unregistered filter throws InvalidOperationException. |
FormActionAttribute |
Sets ViewData[FormActionAttribute.OmitFormTag] to true for the view to read. Nothing in the library reads the flag. |
ModelStateDictionaryExtensions |
Adds the error messages of a Result, or a formatted message, as model errors, and removes several keys at once. |
UrlExtensions.Content(path, toAbsolute) |
Extends IUrlHelper, so views call it as Url.Content(path, true). It resolves a content path and, with toAbsolute, makes it absolute for the helper's current request. When the host carries no port, the absolute URL omits it. |
CustomRouteAttribute |
Declares routes on a controller class. CustomRouteAttribute.RegisterCustomRoutesFromAssembly(app, assembly) maps each attribute of every controller in the assembly as a conventional controller route, ordered by Priority, with the controller and DefaultAction as defaults. A route may not contain {controller}. Attribute routing such as [Route] and [HttpGet("...")] remains the usual ASP.NET Core choice. |
Filters example
This controller exports a temporary file, deletes it after the response is written and logs each call through a filter that the service provider creates. Save it as Controllers/ReportsController.cs:
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.Logging;
using Sphere10.Framework.Web.AspNetCore.MVC;
namespace MvcDemo.Controllers;
public sealed class AuditFilter : IActionFilter {
private readonly ILogger<AuditFilter> _logger;
public AuditFilter(ILogger<AuditFilter> logger) {
_logger = logger;
}
public void OnActionExecuting(ActionExecutingContext context)
=> _logger.LogInformation("Running {Action}", context.ActionDescriptor.DisplayName);
public void OnActionExecuted(ActionExecutedContext context) {
}
}
public sealed class AuditAttribute : FilterFactoryBase<AuditFilter> {
}
[Route("reports")]
public class ReportsController : Controller {
[HttpGet("export")]
[Audit]
[Cleanup]
public IActionResult Export() {
var path = Path.GetTempFileName();
System.IO.File.WriteAllText(path, "Name,Visits\nAlice,3\n");
((List<Action>)ViewBag.Cleanup).Add(() => System.IO.File.Delete(path));
return PhysicalFile(path, "text/csv", "report.csv");
}
}
Register the filter before builder.Build() in Program.cs, because FilterFactoryBase resolves it from the request services:
builder.Services.AddScoped<MvcDemo.Controllers.AuditFilter>();
ViewBag.Cleanup is dynamic, so cast it to List<Action> before you add a lambda. CleanupAttribute runs the delegates after the file result has been sent, so /reports/export downloads the file before it is deleted.
Custom routes example
CustomRouteAttribute maps conventional routes without attribute routing on the controller. Save Controllers/HelpController.cs:
using Microsoft.AspNetCore.Mvc;
using Sphere10.Framework.Web.AspNetCore.MVC;
namespace MvcDemo.Controllers;
[CustomRoute("help/{topic}", DefaultAction = "Topic", Defaults = "topic=index")]
public class HelpController : Controller {
public IActionResult Topic(string topic) => Content($"Help topic: {topic}");
}
Then register the routes of every controller in the assembly, before app.Run() in Program.cs:
using Sphere10.Framework.Web.AspNetCore.MVC;
CustomRouteAttribute.RegisterCustomRoutesFromAssembly(app, typeof(MvcDemo.Controllers.HelpController).Assembly);
/help/paging then returns Help topic: paging, and /help uses the default topic index. Defaults takes key=value pairs joined with &, and DefaultAction adds the action default.
Structure and dependencies
| Folder or file | Contents |
|---|---|
Extensions/ |
Controller, HTML helper, URL, select-list and model-state extensions. |
Forms/ |
Form model, options, scope and result types, and the embedded forms script. |
Filters/ |
CleanupAttribute, FilterFactoryBase<TFilter> and FormActionAttribute. |
Routing/ |
CustomRouteAttribute. |
XML/ |
XmlResult. |
Processing/ |
ImageResult. |
MvcTool.cs |
Tools.Web.Mvc.ToSelectList. |
The project targets net10.0, and also net11.0 when a .NET 11 SDK builds it. It has a Microsoft.AspNetCore.App framework reference, project references to the shared Web.AspNetCore layer and to Drawing, and a Newtonsoft.Json package reference for the form types. The Drawing reference serves only ImageResult. The host-independent Web library suffices when an application needs only the web models.
Upgrading from the combined library
Releases up to 3.1.2 shipped these types inside Sphere10.Framework.Web.AspNetCore, in the Sphere10.Framework.Web.AspNetCore namespace. Reference this package and import Sphere10.Framework.Web.AspNetCore.MVC for the controller and view helpers, forms, filters, routing and results. Replace Tools.Web.AspNetCore.ToSelectList with Tools.Web.Mvc.ToSelectList. The hosting and HTTP types keep their namespaces in the shared package, and the sitemap, navigation and message models moved to Sphere10.Framework.Web.
The forms script is still an embedded resource rather than a static web asset. It moved from the Sphere10.Framework.Web.AspNetCore assembly, where its name was Sphere10.Framework.Web.AspNetCore.Forms.hydrogen-bootstrap-forms-1.0.0.js, to typeof(FormModelBase).Assembly under the name shown above.
Build and verification
From the repository root:
dotnet build src/Sphere10.Framework.Web.AspNetCore.MVC/Sphere10.Framework.Web.AspNetCore.MVC.csproj -p:BuildRevision=0
dotnet test tests/Sphere10.Framework.Web.AspNetCore.Tests -p:BuildRevision=0
The shared web tests check XmlResult status codes and sitemap output, enum list selection and sorting, the embedded script's resource name, asynchronous ImageResult output, FilterFactoryBase resolution, CustomRouteAttribute registration, and BootstrapFormScope completion, form method and overlay options, including a retry after a failed write and asynchronous disposal. The MVC tester is a minimal MVC host with one landing page for further controller and view demonstrations, and it does not include the contact form above. Package validation serves XmlResult output from an external consumer and checks it over HTTP. -p:BuildRevision=0 stops a local build from allocating the next build number.
Related projects
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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. net11.0 is compatible. |
-
net10.0
- Newtonsoft.Json (>= 14.0.1-beta2)
- Sphere10.Framework.Drawing (>= 3.2.0-ci.163.43664782937.1)
- Sphere10.Framework.Web.AspNetCore (>= 3.2.0-ci.163.43664782937.1)
-
net11.0
- Newtonsoft.Json (>= 14.0.1-beta2)
- Sphere10.Framework.Drawing (>= 3.2.0-ci.163.43664782937.1)
- Sphere10.Framework.Web.AspNetCore (>= 3.2.0-ci.163.43664782937.1)
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 |
|---|---|---|
| 3.2.0-ci.163.43664782937.1 | 0 | 11/15/2026 |
| 3.2.0-ci.160.43660265392.1 | 0 | 11/15/2026 |
| 3.2.0-ci.159.43537748063.1 | 51 | 11/14/2026 |
| 3.2.0-ci.158.43536665157.1 | 49 | 11/14/2026 |
| 3.2.0-ci.157.43350632870.1 | 53 | 11/13/2026 |
| 3.2.0-ci.156.43233095581.1 | 48 | 11/12/2026 |