LanguageExt.AspNetCore.NativeTypes
0.2.0
dotnet add package LanguageExt.AspNetCore.NativeTypes --version 0.2.0
NuGet\Install-Package LanguageExt.AspNetCore.NativeTypes -Version 0.2.0
<PackageReference Include="LanguageExt.AspNetCore.NativeTypes" Version="0.2.0" />
paket add LanguageExt.AspNetCore.NativeTypes --version 0.2.0
#r "nuget: LanguageExt.AspNetCore.NativeTypes, 0.2.0"
// Install LanguageExt.AspNetCore.NativeTypes as a Cake Addin #addin nuget:?package=LanguageExt.AspNetCore.NativeTypes&version=0.2.0 // Install LanguageExt.AspNetCore.NativeTypes as a Cake Tool #tool nuget:?package=LanguageExt.AspNetCore.NativeTypes&version=0.2.0
LanguageExt.AspNetCore.NativeTypes
Extensions and middleware for ASP.NET Core that allow you to use LanguageExt types directly in controllers. The 3 main goals of this library are to enable:
- Model binding support for LanguageExt types in controller methods
- Configurable JSON serialization support for LanguageExt types
- Currently, there is only support for
System.Text.Json
. NewtonsoftJson
support will be added eventually.
- Currently, there is only support for
- Transparent
Monad -> IActionResult
mapping to allow defining controller methods in terms of monadic chains[HttpGet] public Eff<string> Hello() => SuccessEff("world");
This library is in early development and should not be used in a production setting.
I've made this mostly to satisfy my own curiosity in how far we can push non-functional code to the edges in C#/AspNetCore.
Installation
Add the nuget package to your project
dotnet add package LanguageExt.AspNetCore.NativeTypes
Call AddLanguageExtTypeSupport()
after AddMvc()
within your ConfigureServices()
method to enable support for LanguageExt types.
var builder = WebApplication.CreateBuilder();
builder.WebHost
.ConfigureServices((IServiceCollection services) =>
{
services
.AddMvc()
.AddLanguageExtTypeSupport() // <-- Add this
;
});
AddLanguageExtTypeSupport
also supports an overload which takes a LanguageExtAspNetCoreOptions
instance. This allows you to configure certain aspects of the model binding system.
var builder = WebApplication.CreateBuilder();
builder.WebHost
.ConfigureServices((IServiceCollection services) =>
{
services
.AddMvc()
.AddLanguageExtTypeSupport(new LanguageExtAspNetCoreOptions {
// override default options
})
;
});
Inbound Type Binding
The following types are supported in controllers as input arguments.
Option<T>
The following binding sources are supported for Option<T>
and the implicit OptionNone
types.
[HttpPost("body")]
public IActionResult FromBody([FromBody] Option<int> num);
// public record RecordWithOptions(Option<int> Num1, Option<int> Num2);
[HttpPost("body/complex")]
public IActionResult FromBodyComplex([FromBody] RecordWithOptions value);
[HttpGet("query")]
public IActionResult FromQuery([FromQuery] Option<int> num);
[HttpGet("route/{num?}")]
public IActionResult FromRoute([FromRoute] Option<int> num);
[HttpGet("header")]
public IActionResult FromHeader([FromHeader(Name = "X-OPT-NUM")] Option<int> num);
[HttpPost("form")]
public IActionResult FromForm([FromForm] Option<int> num);
Collection types
The following LanguageExt collection types are supported:
Seq<T>
Lst<T>
Note: The examples below show only Seq<T>
, but all supported collection types are interchangable.
[HttpPost("body")]
IActionResult FromBody([FromBody] Seq<int> num);
// public record RecordWithSeqs(Seq<int> First, Seq<int> Second);
[HttpPost("body/complex")]
public IActionResult FromBodyComplex([FromBody] RecordWithSeqs value);
[HttpGet("query")]
public IActionResult FromQuery([FromQuery] Seq<int> num);
[HttpGet("route/{num?}")]
public IActionResult FromRoute([FromRoute] Seq<int> num);
[HttpGet("header")]
public IActionResult FromHeader([FromHeader(Name = "X-OPT-NUM")] Seq<int> num);
[HttpPost("form")]
public IActionResult FromForm([FromForm] Seq<int> num);
JSON Serialization
In addition to allowing LangExt types in controller arguments, these types can be returned in JSON return types with configurable representations.
Option
There are 2 serialization strategies available for Option<T>
. You may choose the strategy when calling AddLanguageExtTypeSupport
. All serialization strategies are capable of deserializing Option<T>
from any form. These strategies only affect how serializing Option<T> -> string
is performed.
AsNullable
This strategy uses JSON null
to represent None
and transparently converts Some
values to their normal JSON representations.
This is the default serialization strategy for Option<T>
.
Set with:
new LanguageExtAspNetCoreOptions {
OptionSerializationStrategy = OptionSerializationStrategy.AsNullable
}
Examples:
| Input object | JSON
|-------------- |---------
| Some(7)
| 7
| new {count: Some(7)}
| {count: 7}
| None
| null
| new {count: None}
| {count: null}
Attribution: Initial work for this converter came from this GitHub comment
AsArray
This strategy treats the Option<T>
as a special case array of 0..1
values. This will always produce a JSON array, but will only write a value into the array when in the Some
state.
Set with:
new LanguageExtAspNetCoreOptions {
OptionSerializationStrategy = OptionSerializationStrategy.AsArray
}
Examples:
| Input object | JSON
|-------------- |---------
| Some(7)
| [7]
| new {count: Some(7)}
| {count: [7]}
| None
| []
| new {count: None}
| {count: []}
Attribution: The majority of the work for this converter came from this GitHub comment
Collection types
All collection types (Seq<T>
, etc) that we support from LanguageExt are serialized as arrays, as you would expect.
There are currently no serialization options for these types.
Controller Return Types
Eff/Aff
Returning an Eff<T>
or Aff<T>
from a controller method will cause the effect to be run. Successes will return 200 and return the value in the response. Error cases will return a 500 error by default. This can be customized when registering support for effectful endpoints.
[HttpGet("{id:guid}")]
public Aff<User> FindUser(Guid id) => _db.FindUserAff(user => user.Id == id);
// => Success(User) -> 200 Ok(User)
// => Err -> 500 InternalServerError -or- customized error handler
Your effect can also return action results. This gives you more direct control over the mapping of internal values to results, while still remaining in the monad.
[HttpGet("{id:guid}")]
public Aff<IActionResult> FindUser(Guid id) =>
_db.FindUserAff(user => user.Id == id)
.Map(user => new OkObjectResult(user) as IActionResult)
| @catch(Errors.UserAccountSoftDeleted, _ => SuccessAff<IActionResult>(new NotFoundResult()));
// => Success(User) -> 200 Ok(User)
// => Err(UserAccountSoftDeleted) -> 404 NotFound
// => Err -> 500 InternalServerError -or- customized error handler
To customize global effect result handling, pass a Func<Fin<IActionResult>, IActionResult>
delegate into your call to AddEffAffEndpointSupport
. This delegate is always called when the effect completes. Your delegate should handle both success and failure cases for the effect.
The following example overrides failure handling when the error matches a predefined application error.
Error UserNotFound = Error.New(4009, "User not found");
Func<Fin<IActionResult>, IActionResult> unwrapper = fin =>
fin.Match(
identity,
error => error switch
{
{} err when err == UserNotFound => new NotFoundResult(),
_ => new StatusCodeResult(StatusCodes.Status500InternalServerError),
});
services
.AddMvc()
.AddLanguageExtTypeSupport()
.AddEffAffEndpointSupport(unwrapper)
Using Runtimes
You can also return Eff
/Aff
types which use runtimes from controllers. Doing so requires a little more setup to tell the library how to create the runtime for each call.
We will use the following simple runtime in our examples. For more information on creating and using runtimes, see this wiki article.
// ###########################
// EXAMPLE RUNTIME DEFINITIONS
// ###########################
// Subsystem interface
public interface UsersIO
{
Task<User> FindUser(Func<User, bool> predicate);
}
// Trait for our subsystem
public interface HasUsers<RT>
where RT : struct, HasUsers<RT>
{
Eff<RT, UsersIO> UsersEff { get; }
}
// Convenience functions
public static class Users<RT>
where RT : struct, HasUsers<RT>, HasCancel<RT>
{
public static Aff<RT, User> FindUser(Func<User, bool> predicate) =>
default(RT).UsersEff.MapAsync(async io => await io.FindUser(predicate));
}
// Live runtime definition
public readonly struct Runtime : HasCancel<Runtime>, HasUsers<Runtime>
{
// omitting implementation
}
When setting up Eff/Aff endpoint support in your startup functions, add a call to the overload which requests a runtimeProvider
function. This function will give you an instance of the IServiceProvider
in case you need access to any configured services when constructing your runtime.
Each call to AddEffAffEndpointSupport
adds support for one more runtime type. Calling the non-generic AddEffAffEndpointSupport
adds handlers for the unital runtime (no-runtime Eff<A>
), with each generic call setting up support for a different runtime (Eff<RT1, A>
, Eff<RT2, A>
, etc). If you intend to always use a runtime, you can safely omit the call to the non-generic overload.
Func<Fin<IActionResult>, IActionResult> unwrapper = ... ;
services
.AddMvc()
.AddLanguageExtTypeSupport()
// Adds support for Eff<A> and Aff<A>
.AddEffAffEndpointSupport(unwrapper)
// Adds support for Eff<Runtime, A> and Aff<Runtime, A>
.AddEffAffEndpointSupport<Runtime>(
unwrapper: unwrapper, // If you have a custom unwrapper function, you should also provide that here
runtimeProvider: (IServiceProvider p) => new Runtime() // construct a new runtime or provide an existing one
)
// Adds support for Eff<OtherRuntime, A> and Aff<OtherRuntime, A>
.AddEffAffEndpointSupport<OtherRuntime>(
unwrapper: unwrapper, // If you have a custom unwrapper function, you should also provide that here
runtimeProvider: (IServiceProvider p) => new OtherRuntime()
)
Now we can use the runtime type in our controller method.
[HttpGet("{id:guid}")]
public Aff<Runtime, User> FindUser(Guid id) => Users<Runtime>.FindUser(user => user.Id == id);
// => Success(User) -> 200 Ok(User)
// => Err -> 500 InternalServerError -or- customized error handler
Option
Returning an Option<T>
from a controller method will convert Some
to a 200 and return the value in the response and None
becomes 404NotFound.
[HttpGet("{id:guid}")]
public Option<User> FindUser(Guid id) => _db.Find(user => user.Id == id);
// => Some(User) -> 200 Ok(User)
// => None -> 404 NotFound
Minimal APIs
Please note that minimal API endpoint definitions are currently not supported.
The minimal API system uses a completely different model binding approach that does not allow for injectable deserializers. This means that custom deserialization/binding must be defined directly on the type. This would require defining these parsing/binding rules directly in the LanguageExt type definitions, which does not seem like a worthwhile exercise.
Luckily, it seems this limitation is a pain point for others and a feature request to add injectable binding is being tracked in this github issue.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 is compatible. net5.0-windows was computed. net6.0 was computed. 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. |
-
net5.0
- LanguageExt.Core (>= 4.4.6)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on LanguageExt.AspNetCore.NativeTypes:
Package | Downloads |
---|---|
LanguageExt.AspNetCore.NativeTypes.NewtonsoftJson
Enables controlling Newtonsoft.Json conversion of LanguageExt types. |
GitHub repositories
This package is not used by any popular GitHub repositories.
## [0.2.0] - 2023-10-09
### Added
- Added support for configuring this library from IMvcCoreBuild instances
### Changed
- Improved API for configuring features of this library