Flowsy.Web.Api 12.2.1

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

// Install Flowsy.Web.Api as a Cake Tool
#tool nuget:?package=Flowsy.Web.Api&version=12.2.1

Flowsy Web API

Foundation components for Web APIs.

Features

This package gathers and extends the tools needed to create solid Web APIs by covering the following aspects:

  • API Versioning
  • API Key Security
  • Routing Naming Convenion
  • Request Validation
  • Mediator Pattern for Controllers
  • Data Streaming
  • Logging
  • Problem Details
  • Swagger Documentation with Schema & Operation Filters

Dependencies

Flowsy Web API relies on other Flowsy packages as well as other excellent libraries well known by the community.

Startup

Add the following code to the Program.cs file and customize as needed:

using System.Globalization;
using System.Reflection;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using Flowsy.Mediation;
using Flowsy.Web.Api.Documentation;
using Flowsy.Web.Api.Exceptions;
using Flowsy.Web.Api.Routing;
using Flowsy.Web.Api.Security;
using Flowsy.Web.Api.Versioning;
// using Flowsy.Web.Localization; // Add a reference to Flowsy.Web.Localization to add localization support
using FluentValidation;
using Hellang.Middleware.ProblemDetails;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Serilog;

//////////////////////////////////////////////////////////////////////
// Build the application
//////////////////////////////////////////////////////////////////////

var builder = WebApplication.CreateBuilder(args);
var myApiAssembly = Assembly.GetExecutingAssembly();

// Load default culture from configuration
CultureInfo.CurrentUICulture = new CultureInfo("en-US");

// Add logging
builder.Host.UseSerilog((_, loggerConfiguration) =>
    {
        loggerConfiguration
            .ReadFrom
            .Configuration(builder.Configuration, "Some:Configuration:Section")
            .Destructure.ByTransforming<ClaimsPrincipal>(p => p.Identity?.Name ?? p.ToString() ?? "Unknown user")
            .Destructure.ByTransforming<ApiClient>(c => c.ClientId)
            .Destructure.ByTransforming<CultureInfo>(c => c.Name);
            // Add other transformations as needed
    
        // Customize loggerConfiguration
    });

// Add API Versioning
builder.Services.AddApiVersioning("1.0");
    
// Add a reference to Flowsy.Web.Localization to add localization support
// builder.AddLocalization(options => {
//     // Load supported culture names from configuration
//     options.SupportedCultureNames = new [] { "en-US", "es-MX" };
// });

// Add CORS policy
builder.Services.AddCors(options =>
    {
        options.AddDefaultPolicy(policy =>
        {
            // Load settings from configuration
            policy.WithOrigins("https://www.example1.com", "https://www.example2.com");
            policy.WithMethods("OPTIONS", "GET", "POST", "PUT", "PATCH", "DELETE");
            policy.WithHeaders("Accept-Content", "Content-Type");
        });
    });

// Add controllers and customize as needed
builder.Services
    .AddControllers(options =>
    {
        options.Conventions.Add(new RouteTokenTransformerConvention(new KebabCaseRouteParameterTransformer()));
    })
    .AddJsonOptions(options =>
    {
        options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
        options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
        options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    });

// Configure form options
builder.Services.Configure<FormOptions>(options =>
    {
        // Customize options
    });

// Add API client management
builder.Services.AddSingleton<IApiClientManager>(serviceProvider => {
    var clients = new List<ApiClient>();
    // Load clients from some data store or provide a custom IApiClientManager implementation
    return new InMemoryApiClientManager(clients);
});

// Add FluentValidation and customize as needed
ValidatorOptions.Global.LanguageManager.Enabled = languageManagerEnabled;
ValidatorOptions.Global.PropertyNameResolver = (_, member, _) => member?.Name.ApplyNamingConvention(propertyNamingConvention);
builder.Services.AddValidatorsFromAssembly(myApiAssembly);

// Add mediation
builder.Services.AddMediation(
    true, // Register InitializationBehavior to set the current user and culture for every request
    true, // Register LoggingBehavior to log information for every request and its result
    myApiAssembly // Register queries and commands from this assembly
    // Register queries and commands from others assemblies
    );

// Add other services

// Add Problem Details and customize as needed
builder.Services.AddProblemDetails(options =>
    {
        var isProduction = builder.Environment.IsProduction();

        options.IncludeExceptionDetails = (context, exception) => !isProduction;

        options.ShouldLogUnhandledException = (context, exception, problemDetails) => true;
        
        // Map different exception classes to specific HTTP status codes
        options.Map<SomeException>(exception => exception.Map(StatusCodes.Status500InternalServerError, !isProduction));
    });

// If not in production environment, add Swagger documentation with request examples from the executing assembly
if (!builder.Environment.IsProduction())
    builder.AddDocumentation(myApiAssembly);


//////////////////////////////////////////////////////////////////////
// Configure the HTTP request pipeline and run the application
//////////////////////////////////////////////////////////////////////

var app = builder.Build();

app.UseProblemDetails();

if (!app.Environment.IsDevelopment())
    app.UseHttpsRedirection();

if (!app.Environment.IsProduction())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app
    .UseApiVersioning()
    .UseSerilogRequestLogging(options =>
    {
        // Customize options if needed
    });
    
app.UseCors();

// Add a reference to Flowsy.Web.Localization to add localization support
// app.UseLocalization();

app.UseAuthentication();
app.UseRouting();
app.MapControllers();

// Add custom middleware
// app
    // .UseMiddleware<SomeMiddleware>()
    // .Use((context, next) =>
    // {
    //     // Perform some task
    //     return next(context);
    // });

app.UseAuthorization();   

app.Run();

Controllers

This package provides the MediationController class to offer built-in validation and mediation functionallity based on FluentValidation.AspNetCore and Flowsy.Mediation.

Our goal is to put the application logic out of controllers, even in separate assemblies.

using System.Threading;
using Flowsy.Web.Api.Mediation;
using Flowsy.Web.Api.Security;
using Microsoft.AspNetCore.Mvc;
using My.Application.Commands;

[ApiController]
[Route("/api/[controller]")]
[AuthorizeApiClient("ClientId1", "ClientId2")] // Only for specific API clients with a valid API Key 
// [AuthorizeApiClient] // For any API client with a valid API Key 
public class CustomerController : MediationController
{
    // With manual validation and using the IMediator instance directly
    // The mediation result is and instance of the expected CreateCustomerCommandResult class 
    [HttpPost]
    public async Task<IActionResult> CreateAsync([FromBody] CreateCustomerCommand command, CancellationToken cancellationToken)
    {
        var validationResult = await ValidateAsync(command, cancellationToken);
        if (!validationResult.IsValid)
            return ValidationProblem(validationResult);
            
        var commandResult = await Mediator.Send(command, cancellationToken);
        return Ok(commandResult);
    }
    
    // With automatic validation if an instance of IValidator<UpdateCustomerCommand> is registered
    // The mediation result is and instance of the expected UpdateCustomerCommandResult class
    // You must call AddRequestValidation on MediationBuilder when adding mediation to the application services. 
    [HttpPut("{customerId:int}")]
    public async Task<IActionResult> UpdateAsync(int customerId, [FromBody] UpdateCustomerCommand command, CancellationToken cancellationToken)
    {
        command.CustomerId = customerId; // Ensure the command is using the right customer ID
        var commandResult = await Mediator.Send(command, cancellationToken);
        return Ok(commandResult);
    }
    
    // With automatic validation if an instance of IValidator<DeleteCustomerCommand>
    // The mediation result is an instance of IActionResult 
    [HttpDelete("{customerId:int}")]
    public Task<IActionResult> DeleteAsync(int customerId, [FromBody] DeleteCustomerCommand command, CancellationToken cancellationToken)
    {
        command.CustomerId = customerId; // Ensure the command is using the right customer ID
        return MediateAsync<DeleteCustomerCommand, DeleteCustomerCommandResult>(command, cancellationToken);
    }
}

Data Streaming

1. Configure File Buffering Options and Register a Buffering Provider

// Program.cs
// using ...
using Flowsy.Web.Api.Streaming.Buffering;
// using ...

var builder = WebApplication.CreateBuilder(args);
// Add services
builder.Services.Configure<FileBufferingOptions>(options =>
    {
        // Configure options:
        // MemoryThreshold
        // BufferLimit
        // TempFileDirectory
        // TempFileDirectoryAccessor
        // BytePool
    });
builder.Services.AddSingleton<IBufferingProvider, BufferingProvider>()

var app = builder.Build();
// Use services
app.Run();

2. Read or Write Streams Using a Buffering Provider

// FileUploader.cs
// using ...
using Flowsy.Web.Api.Streaming.Buffering;
// using ...

public class FileUploader
{
    private readonly IBufferingProvider _bufferingProvider;
    
    public FileUploader(IBufferingProvider bufferingProvider)
    {
        _bufferingProvider = bufferingProvider;
    }
    
    public void UploadLargeFile(Stream inputStream)
    {
        using var bufferingStream = _bufferingProvider.CreateFileBufferingReadStream(inputStream);
        // Read content using bufferingStream 
        // Make decisions based on the content
        bufferingStream.Seek(0, SeekOrigin.Begin); // Rewind
        // Read content again and store it somewhere
    }
}

Multipart Content

The following example shows how to read data from a multipart request. If an instance of IBufferingProvider is registered, the MultipartHandler service will use it to buffer content while reading request body sections.

using System.Threading;
using Flowsy.Web.Api.Streaming.Multipart;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("/api/[controller]")]
public class ExampleController : ControllerBase // Or MediationController
{
    private readonly IMultipartHandler _multipartHandler;
    
    public ExampleController(IMultipartHandler multipartHandler)
    {
        _multipartHandler = multipartHandler;
    }

    [HttpPost]
    public async Task<IActionResult> Post(CancellationToken cancellationToken)
    {
        // The MultipartContent instance will be disposed after return
        await using var multpartContent = await _multipartHandler.GetContentAsync(Request, cancellationToken);
        
        // Loop through request fields
        foreach (var (key, values) in multpartContent.Data)
        {
            // Process each field
            foreach (var value in values)
            {
                // Process each field value
            }
        }

        // Loop through request files
        foreach (var (key, multipartFile) in multpartContent.Files)
        {
            // Process each multipart file
        }
        
        // Deserialize fields expected to be in JSON format
        var myObject = multpartContent.DeserializeJsonField<MyClass>("fieldName");
        
        return Ok(/* Some result */);
    }
}

Security Extensions

using System.Threading;
using Flowsy.Web.Api.Forms;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("/api/[controller]")]
public class ExampleController : ControllerBase // Or MediationController
{
    [HttpPost]
    public IActionResult Post()
    {
        // Obtain the client identifier and API Key from a header named X-{ClientId}-ApiKey
        HttpContext.GetApiKey(out var clientId, out var apiKey);
        // Do something with clientId and apiKey
        
        // Obtain value from a header named Athorization without the 'Bearer ' prefix
        var authorization = HttpContext.GetHeaderValue("Authorization", "Bearer ");
        // Do something with authorization
        
        return Ok(/* Some result */);
    }
}
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.

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
12.2.1 224 11/28/2023
12.2.0 95 11/27/2023
12.1.7 142 10/5/2023
12.1.6 91 10/5/2023
12.1.5 101 10/4/2023
12.1.4 108 10/4/2023
12.1.3 92 10/4/2023
12.1.2 115 9/27/2023
12.1.1 125 9/15/2023
12.1.0 114 9/15/2023
12.0.0 149 8/29/2023
11.1.0 142 8/29/2023
11.0.1 129 8/28/2023
11.0.0 108 8/28/2023
10.1.9 252 6/2/2023
10.1.8 134 5/24/2023
10.1.7 156 5/8/2023
10.1.6 217 3/25/2023
10.1.5 287 3/10/2023
10.1.4 208 3/10/2023
10.1.3 192 3/10/2023
10.1.2 186 3/9/2023
10.1.1 207 3/9/2023
10.1.0 197 3/9/2023
10.0.1 210 3/9/2023
10.0.0 206 3/9/2023
9.1.1 249 2/27/2023
9.1.0 236 2/24/2023
9.0.0 233 2/24/2023
8.0.1 246 2/22/2023
8.0.0 231 2/21/2023
7.1.2 249 2/21/2023
7.1.1 216 2/21/2023
7.1.0 221 2/21/2023
7.0.10 225 2/21/2023
7.0.9 213 2/21/2023
7.0.8 242 2/8/2023
7.0.7 252 2/8/2023
7.0.6 284 1/15/2023
7.0.5 372 12/8/2022
7.0.4 291 12/4/2022
7.0.3 291 12/4/2022
7.0.2 305 12/4/2022
7.0.1 306 11/20/2022
7.0.0 318 11/17/2022
6.0.0 337 11/10/2022
5.0.1 331 11/8/2022
5.0.0 321 11/7/2022
4.0.0 334 11/7/2022
3.1.1 356 11/6/2022
3.1.0 341 11/6/2022
3.0.2 344 11/6/2022
3.0.1 334 11/6/2022
3.0.0 331 11/6/2022
2.0.2 369 11/4/2022
2.0.1 335 11/3/2022
2.0.0 326 11/3/2022
1.0.1 347 11/3/2022
1.0.0 335 11/3/2022