Mundane 6.0.0

dotnet add package Mundane --version 6.0.0                
NuGet\Install-Package Mundane -Version 6.0.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="Mundane" Version="6.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Mundane --version 6.0.0                
#r "nuget: Mundane, 6.0.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.
// Install Mundane as a Cake Addin
#addin nuget:?package=Mundane&version=6.0.0

// Install Mundane as a Cake Tool
#tool nuget:?package=Mundane&version=6.0.0                

<img align="left" width="116" src="https://raw.githubusercontent.com/adambarclay/mundane/main/build/Mundane.png"/>

Mundane

License: MIT nuget build coverage

Mundane is a lightweight "no magic" web framework for .NET.

    routeConfiguration.Get("/hello-world", () => Response.Ok(o => o.Write("Hello World!"))));

Getting Started

For ASP.NET, install the Mundane.Hosting.AspNet nuget package, then in your ASP.NET startup code call app.UseMundane();, passing in the routing and dependencies configuration.

Example

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        var dependencies = new Dependencies(
            new Dependency<Configuration>(new Configuration(env)),
            new Dependency<DataRepository>(request => new DataRepositorySqlServer(
                request.Dependency<Configuration>().ConnectionString)));

        var routing = new Routing(
            routeConfiguration =>
            {
                routeConfiguration.Get("/", HomeController.HomePage);
                routeConfiguration.Get("/data/{id}", DataController.GetData);
                routeConfiguration.Post("/data/{id}", DataController.UpdateData);
            });

        app.UseMundane(dependencies, routing);
    }

Request/Response Model

Mundane uses a simple request/response model to handle HTTP requests. There is no automatic dependency injection or model validation, so all request parameters and dependencies must be retrieved from the Request object. Passing the Request object to the endpoint is optional but the endpoint must return a Response object.

Example

    internal static class HomeController
    {
        internal static Response HomePage()
        {
            return Response.Ok(o => o.Write(Views.HomePage));
        }
    }

    internal static class DataController
    {
        internal static async ValueTask<Response> GetData(Request request)
        {
            var dataRepository = request.Dependency<DataRepository>();

            var data = await dataRepository.GetData(request.Route("id"));
			
            return Response.Json(async o => await JsonSerializer.SerializeAsync(o.Stream, data));
        }
    }

The Request

The Request object contains all of the parameters passed to the request, and the dependencies registered at startup.

    routeConfiguration.Get("/example/{id}", request =>
    {
        var cookieValue = request.Cookie("cookieName");
        var uploadedFile = request.File("fileParameterName");
        var formValue = request.Form("formParameterName");
        var headerValue = request.Header("headerName");
        var queryValue= request.Query("queryParameterName");
        var routeParameter = request.Route("id");

        var dataRepository = request.Dependency<DataRepository>();

        return Response.Ok();
    }

All of the request parameter methods return empty string if the parameter was not sent (or FileUpload.Unknown in the case of request.File()). It is possible to check if a parameter was sent with the *Exists() methods.

    routeConfiguration.Get("/example", request =>
    {
        var cookieExists = request.CookieExists("cookieName");
        var uploadedFileExists = request.FileExists("fileParameterName");
        var formExists = request.FormExists("formParameterName");
        var headerExists = request.HeaderExists("headerName");
        var queryExists = request.QueryExists("queryParameterName");

        return Response.Ok();
    }

The Response

The Response object is made up of two parts, the header and the body.

The header includes the HTTP status code, and the HTTP response headers.

The body is supplied to the Response constructor as a lambda expression, which is executed after the response is returned, to guarantee the body is only written to the output after all of the headers have been set.

Status Code

The Response can be constructed with a custom status code value

    return new Response(200, o => o.Write("Eveything is OK!"));

or by using one of the static helper methods on the Response object.

    // Status code 200, content-type=text/html;charset=utf-8
    return Response.Ok(o => o.Write("Eveything is OK!"));
Response Headers

Response headers can be added using the AddHeader() method and either a custom HeaderValue e.g. new HeaderValue("MyHeader", "MyValue"), or by using the static helper methods of HeaderValue.

    return Response.File(o => o.Write(imageData.Bytes)), "image/png")
        .AddHeader(HeaderValue.CacheControl("public, max-age=31536000"))
        .AddHeader(HeaderValue.LastModified(imageData.LastModified));
Cookies

Cookies are HTTP headers so they are also added with the AddHeader() method.

    internal static async ValueTask<Response> Authenticate(Request request)
    {
        var userRepository = request.Dependency<UserRepository>();

        var user = await userRepository.FindUser(request.Form("userName"), request.Form("password"));

        if (user == User.Unknown)
        {
            return Response.Unauthorized()
                .AddHeader(HeaderValue.DeleteCookie("auth"));
        }

        return Response.RedirectSeeOther("/my-account")
            .AddHeader(HeaderValue.PersistentCookie(
                "auth",
                userRepository.GenerateAuthToken(user),
                TimeSpan.FromHours(24)));
    }

Routing

Route Configuration

Mundane has four endpoint signatures:

    Response Endpoint();
    Response Endpoint(Request request);
    ValueTask<Response> Endpoint();
    ValueTask<Response> Endpoint(Request request);

Endpoints are added to the Routing configuration using RouteConfiguration. The HTTP methods DELETE, GET, POST, and PUT are supported, along with any custom method using Endpoint(). A custom 404 handler can optionally be specified as the second parameter of the Routing constructor.

    new Routing(
        routeConfiguration =>
        {
            routeConfiguration.Delete("/delete", () => Response.Ok());
            routeConfiguration.Get("/get", request => Response.Ok());
            routeConfiguration.Post("/post", () => ValueTask.FromResult(Response.Ok());
            routeConfiguration.Put("/put", request => ValueTask.FromResult(Response.Ok());
            routeConfiguration.Endpoint("PATCH", "/patch", PatchController.Patch);
        },
        NotFoundController.NotFound);

Routes

Routes must begin with a forward slash "/".

A route with a trailing slash will be treated differently to a route without a trailing slash,
e.g. "/my-route" and "/my-route/" are two different routes.

Capture Parameters

Routes can include capture parameters specified with curly braces e.g. "/my-route/{id}".

When a request is made to "/my-route/123", request.Route("id") == "123".

Greedy Capture Parameters

Routes can also include greedy capture parameters, which will capture the rest of the URL, including any additional forward slashes.

Greedy parameters are specified with an asterisk before the closing curly brace e.g. "/my-route/{path*}".

When a request is made to "/my-route/123/456", request.Route("path") == "123/456".

Greedy capture parameters can only appear once in a route and must be at the end.

Multiple Capture Parameters

A route may contain any number of capture parameters, as long as there is no more than one greedy parameter.

    routeConfiguration.Get("/shop/{productType}/{id}", ProductController.ShowProduct);
    routeConfiguration.Get("/shop/{productType}/search/{searchTerm*}", SearchController.Search);
Custom Routing

Mundane routing is deliberately kept simple. If more complicated routing is required, use a greedy capture parameter and your own route processing.

    routeConfiguration.Get(
        "/{customRouting*}",
        request =>
        {
            var routeParameters = MyCustomRouting.Execute(request.Route("customRouting"));

            if (routeParameters.IsAFooRoute)
            {
                return FooController.Foo(request, routeParameters);
            }
            
            if (routeParameters.IsABarRoute)
            {
                return BarController.Bar(request, routeParameters);
            }
        });

Dependencies

The Request object uses the DependencyFinder interface to find registered dependencies. Find() is passed the current Request object in case any dependencies rely on parameters contained in the request.

    public interface DependencyFinder
    {
        T Find<T>(Request request);
    }

Mundane comes with a default implementation of DependencyFinder called Dependencies.

The constructor for Dependencies takes zero or more Dependency<T> objects as parameters.

    var dependencies = new Dependencies(
        new Dependency<Configuration>(new Configuration(env)),
        new Dependency<DataRepository>(request => new DataRepositorySqlServer(
            request.Dependency<Configuration>().ConnectionString)),
        new Dependency<EmailSender>(() => new EmailSenderSmtp()));

The Dependency<T> type parameter is the type which will be requested (often an interface or a base class).

The Dependency<T> constructor parameter is either an object instance (which will exist for the whole lifetime of the application), or a lambda expression to create an instance of the requested type, which will create a new instance each time it is called.

The lambda expression optionally takes the current Request as a parameter.

Dependencies does not automatically find dependencies of the object being created, so chained dependencies must be requested explicitly by calling request.Dependency() during creation.

Validation

Mundane includes some helpers to validate parameters in the request.

In this example for updating a user profile, the controller makes use of the Validator class to convert the parameters in the request into a command object.

    internal static class ProfileController
    {
        // POST /profile/edit
        internal static async ValueTask<Response> UpdateProfile(Request request)
        {
            (var invalid, var updateProfileCommand) = Validator.Validate(
                validator => new UpdateProfileCommand(request, validator));

            if (invalid)
            {
                // Show the input form prefilled with the values and error messages in updateProfileCommand.
                return Response.Ok(...);
            }

            var profileRepository = request.Dependency<ProfileRepository>();

            await profileRepository.UpdateProfile(updateProfileCommand);

            // Success.
            return Response.RedirectSeeOther("/profile");
        }
    }

In this example, the validation takes place in the command object constructor by calling the Validate() method.

The Validated<T> properties contain the validated value and the list of errors which occurred for that property during validation.

    internal sealed class UpdateProfileCommand
    {
        // Initalise all of the validated properties to strings which will be displayed on the form.
        internal UpdateProfileCommand()
        {
            this.Name = string.Empty;
            this.Email = string.Empty;
            this.FavouriteNumber = (0, string.Empty); // Initial value with initial display string.
            /* etc... */
        }

        internal UpdateProfileCommand(Request request, Validator validator)
        {
            this.Name = validator.Value(request.Form("Name"))
                .Validate(name => name.Length > 0, "You must enter a name.")
                .Validate(name => name.Length <= 200, "Name must be no more than 200 characters.");

            this.Email = validator.Value(request.Form("Email"))
                .ValidEmail();

            // Value() can also convert from string to another type.
            this.FavouriteNumber = validator.Value(
                request.Form("FavouriteNumber"),
                int.Parse,
                0,
                "Favourite number must be an integer");

            /* etc... */
        }

        internal Validated<string> Name { get; }

        internal Validated<string> Email { get; }

        internal Validated<int> FavouriteNumber { get; }

        /* etc... */
    }

Use extension methods to create custom reusable validation methods.

    internal static class ValidationExtensions
    {
        internal static Validated<string> ValidEmail(this Validated<string> validated)
        {
            return validated.Validate(
                email => email.Length > 0 &&
                    email.Length <= 254 &&
                    email.Contains('@', StringComparison.Ordinal) &&
                    email.All(c => !char.IsWhiteSpace(c)),
                "You must supply a valid email address.");
        }
    }
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net6.0

    • No dependencies.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Mundane:

Package Downloads
Mundane.ViewEngines.Mustache

Mundane is a lightweight "no magic" web framework for .NET. This package provides Mundane with a view engine using a Mustache-like syntax.

Mundane.Hosting.AspNet

Mundane is a lightweight "no magic" web framework for .NET. This package enables a Mundane application to be hosted with ASP.NET.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
6.0.0 690 11/11/2021
5.0.3 750 9/5/2021
5.0.2 675 6/3/2021
5.0.1 311 6/3/2021
5.0.0 408 5/4/2021
4.0.0 411 2/17/2021
3.0.0 457 2/2/2021
2.0.0 496 12/12/2020
1.0.1 471 11/23/2020
1.0.0 467 11/17/2020