/Orleans.Http

An HTTP API for Microsoft Orleans

Primary LanguageC#

Orleans.Http

Orleans.Http

Orleans HTTP Endpoints

Build Package Version NuGet Downloads Gitter

Orleans is a framework that provides a straight-forward approach to building distributed high-scale computing applications, without the need to learn and apply complex concurrency or other scaling patterns.

Orleans.Http is a package that use ASP.Net Core as a frontend endpoint for Orleans grains without the need of controller classes.

This package leverages ASP.NET Core Endpoint Routing to expose grains as HTTP endpoints without need of have to implement Controllers. The request is received and processed by grains methods itself. The idea is to perform in a similar way to Controllers without having to add any boilerplate code to your Orleans project.

At the silo startup, the package look for usage of [Route] and [HttpXXX] attributes onn grain interface methods and register a route on ASP.NET Core endpoints route table. When a request arrive to that route, the parameters are extracted from the request, mapped to grain method parameters, and the grain is invoked. If the grain return Task<T>, the returning object is serialized back to the caller using one of the registered IMediaTypeHandler and added to the Response body.

Installation

Installation is performed via NuGet

On your Silo Project

From Package Manager:

PS> Install-Package Orleans.Http -prerelease

.Net CLI:

# dotnet add package Orleans.Http -prerelease

Paket:

# paket add Orleans.Http -prerelease

On your Grain Interface Project(s)

From Package Manager:

PS> Install-Package Orleans.Http.Abstractions -prerelease

.Net CLI:

# dotnet add package Orleans.Http.Abstractions -prerelease

Paket:

# paket add Orleans.Http.Abstractions -prerelease

Serialization and parameter mapping

The serialization of both request and response body is handled by implementations of IMediaTypeHandler. The runtime checks the Content-Type header of the request and lookup for a IMediaTypeHandler registered implementation that can deserialize the body with that partticular media type. If one matches, the deserialization happens and the deserialized object is passed to the parameter of the grain method being invoked that is marked with the [FromBody] attribute. If there is no serializer registered for that Content-Type, it will just set the parameter as null.

Whenever the method returns Task<T>, the runtime will check if there is an Accept header and use it to lookup for a IMediaTypeHandler that will then serialize the response back to the caller in the Response body. If there is no Accept header, it will then try to serialize the response using the Content-Type header instead.

Parameters can be also mapped from both the Route and the Query string by using [FromRoute] and [FromQuery] parameter attributes respectively. Those attributes works only for primitive types.

By default, Json (using System.Text.Json), XML and Forms are provided with Orleans.Http package. Protobuf (protobuf-net) is also available by adding the Orleans.Http.MediaTypes.Protobuf package.

Custom Serializers

If you want to implement your own IMediaTypeHandler to handle a new (or existig) media type, just implement and add an instance of the following interface to the DI context and it will be invoked by the runtime:

public interface IMediaTypeHandler
{
    string MediaType { get; } // This should be a unique media type i.e. application/mystuff
    ValueTask Serialize(object obj, PipeWriter writer);
    ValueTask<object> Deserialize(PipeReader reader, Type type, CancellationToken cancellationToken);
}

Authentication / Authorizattion

In a similar way to ASP.NET Core Controllers, this package also allow the developer to leverage ASP.NET Core Authentication/Authorization middleware. Just add [Authorize] attribute to the grain method interface and it will just work the same way. Make sure to configure your ASP.NET Core Authentication/Authorizattion middleware.

Routes and Attributes

The routes are generated in a similar way as ASP.NET Core Attribute Routing. By default, no route is generated and all grains are considered private.

The Pattern property of the attributes define the route to be used. If the Pattern property is ommited, the default route is used as the following pattern:

{optionalPrefix}/{optionalTopLevelPattern}/{grainTypeName}/{grainId}/{methodName}

By default, if an attribute Pattern property is set, it is required to somewhere into the pattern to add the {grainId} string otherwise the route will fail to be registered. Optionally, you can also add {grainIdExtension} in case of grains that have Compound keys.

The following attributes are under the Orleans.Http.Abstractions package:

Note: All routes described by the attribute takes into consideration the optional prefix set on yor Startup class when you call MapGrains("prefix").

[Route]

This attribute can be used on both interface and methods.

When applied to a Grain Interface, this attribute Pattern property value is added as the first node of the grain route.

When added to a Grain Method, this attribute tells the runtime to route ALL HTTP verbs to that particular Pattern for that method.

[HttpXXX]

This attribute can be applied only to methods.

The value of Patttern property is used to generate the route of that method for a particular HTTP Verb. If the patttern starts with /, it will ignore all the prefixes and will be used as the de-facto route for that method.

routeGrainProviderPolicy optional argument

This argument allows you to set the route grain provider used for the speicified endpoint. This allows you to override the default behaviour and provide an IGrain that the request will go to. To register a route grain provider policy, use the extension method UseRouteGrainProviders(Action<IRouteGrainProviderPolcyBuilder>) on IApplicationBuilder In your Startup class. The default RouteGrainProvider policy may also be set there. Registering the policy takes in a generic type argument, this type must implement IRouteGrainProvider, optionally, if your implementing type is registered with dependency injection, it will use that type's lifetime, otherwise it will have a scoped lifetime by default.

Example usage

Using the .Net Core Generic Host configure Orleans the same way you would and add the ASP.NET Core to it:

    var hostBuilder = new HostBuilder();
    hostBuilder.UseConsoleLifetime();
    hostBuilder.ConfigureLogging(logging => logging.AddConsole());
    hostBuilder.ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseUrls("http://*:9090");
        webBuilder.UseStartup<Startup>();
    });

    hostBuilder.UseOrleans(b =>
    {
        b.UseLocalhostClustering();

        b.ConfigureApplicationParts(parts => parts.AddApplicationPart(typeof(Startup).Assembly));
    });

    var host = hostBuilder.Build();
    host.Start();

On your Startup class:

public class Startup
{
    public const string SECRET = "THIS IS OUR AWESOME SUPER SECRET!!!";
    public void ConfigureServices(IServiceCollection services)
    {
        // Inject IHttpContextAccessor on your grain to get access to the HttpContext
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

        // Add the authentication services if you want to use it. 
        // For this example, we're using a simple Jwt
        services.AddAuthentication(opt =>
        {
            opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
        .AddJwtBearer(opt =>
        {
            opt.RequireHttpsMetadata = false;
            opt.SaveToken = true;
            opt.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuerSigningKey = true,
                IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SECRET)),
                ValidateIssuer = false,
                ValidateAudience = false
            };
        });

        services.AddAuthorization();

        // Add the GrainRouter service and any IMediaTypeHandler instances you want
        services
            .AddGrainRouter()
            .AddJsonMediaType()
            .AddProtobufMediaType();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // Enable ASP.NET Core Endpoint Routing
        app.UseRouting();

        // Enable Authentication
        app.UseAuthentication();

        // Enable Authorization
        app.UseAuthorization();

        // Configure ASP.NET Core endpoints
        app.UseEndpoints(endpoints =>
        {
            // Call MapGrains([prefix]) with an optional prefix for the routes.
            endpoints.MapGrains("grains");

            // You can add any other endpoints here like regular ASP.NET Controller, SignalR, you name it. 
            // Orleans endpoints are agnostic to other routes.
        });

        //Optionally register route grain provider policies
        app.UseRouteGrainProviders(routeGrainProviders =>
        {
            routeGrainProviders.RegisterRouteGrainProvider<YourCustomRouteGrainProvider>("SomePolicy");

            //You may also optionally set the defaulty policy, so it it used by every endpoint without a specified policy
            routeGrainProviders.SetDefaultRouteGrainProviderPolicy("SomePolicy");
        });
    }
}

Contributions

PRs and feedback are very welcome!