Writing REST Services - Action Basics
This tutorial will teach you the basics of creating a web service using REST api (over http).
Prerequisites
- Windows 10/11
- Visual studio 2019/2022
- Phoesion Glow Blaze , with Reactor running (see getting started guide)
Sample Code
You can download the Sample Code archive from the downloads section.
This tutorial follows the 1_REST sample in the archive. (View code on GitHub)
Routes
when trying to consume an action from a web-browser/rest-client, the basic Uri rule you can use is :
- http[s]://hostname/ServiceName/[v0]/ModuleName/[v0]/ActionName/[OperationId]/[params][?queryString]
Where fields in brackets [xx] are optional.
Naming conventions
To make it easier to keep service/module names clean, but still have efficient routing rules, some naming conventions/shortcuts can be used.
Service naming conventions
Unless specified otherwise, the assembly's name will be used for accessing the service, eg for an assembly HelloWorld.dll the routes will be http://hostname/HelloWorld/... .
To specify a service name you can use the [ServiceName] attribute on your ServiceMain class.
For example :
[ServiceName("HelloWorld")]
public class ServiceMain : FireflyService
Module naming conventions
In general, the module's relative namespace and class name will be used for accessing it's actions, eg for an class Modules/Greeter.cs the routes are *http://hostname/ServiceName/Greeter/...
- The Modules. prefix in the class's namespace can be omitted for modules.
- The xxxModule suffix in the class's name can be omitted, eg for a class GreeterModule.cs the route can be http://hostname/ServiceName/Greeter/..
- The same module can also be accessed using it's full relative namespace like http://hostname/ServiceName/Modules.Greeter/... .
REST Methods
You specify one or more REST Methods you want your Action to be available, using the Methods flags in the [Action] attribute.
For example :
[Action(Methods.GET)]
public string Action1() { return "This is a GET action"; }
or
[Action(Methods.POST)]
public string Action1() { return "This is a POST action"; }
or for multiple methods
[Action(Methods.GET | Methods.POST)]
public string Action1() { return "This can be hit from both a GET and a POST request"; }
Action Context
When you need more information about the context in which an action is called, you can use the Context property.
There are a few Context types that you can use depending on the level of information you need, but in general you have the generic IActionContext Context and the more specific IPhotonRequest Request or any of the derived photon types (eg IPhotonActionRequest Request )
For example :
[Action(Methods.GET)]
public string Action2()
{
return $"Called Action2 from ip {Request.SourceIP} " +
$"with Path = '{string.Join("/", Request.Path)}' " +
$"and QueryString = '{string.Join("/", Request.Query.Select(kv => kv.Key + "=" + kv.Value))}'";
}
or using action context
[Action(Methods.GET)]
public string Action2()
{
return $"Called Action2 from ip {Context.Request.SourceIP}";
}
QueryString parameters
Sending parameters to your REST Action can be done using the QueryString. The key-value parameters of the query string will be automatically parsed and mapped to the method parameters of your Action.
For example, assuming we have the Action :
[Action(Methods.GET)]
public string Greet(string name, int age)
{
return $"Hello {name}. Your age is {age} ";
}
you can call it with : http://xxxxx/MyService/MyModule/Greet?name=John&age=20
and get the result :
Hello John. Your age is 20
Data Model Serialization / Deserialization
The Phoesion Glow system will automatically handle the deserialization of the request and the serialization of the response based on the context of the request.
Let's take for example the following action :
[Action(Methods.POST)]
public MyResponse DoTheThing([FromBody] MyRequest Model)
{
return new MyResponse()
{
IsSuccess = true,
Message = $"Hello {Model.InputName}",
};
}
In a REST Request :
- For the deserialization of the body, the 'Content-Type' header will be examined to determine the deserialization method.
- If the Content-Type is 'application/json' the body will deserialized using a json deserializer.
- An other Content-Type can be 'application/xml' and in this case an Xml deserializer will be used.
- For the serialization of the response, the 'Accept' header will be examined the serialization method based on the client expectations.
- If the Accept type is 'application/json' the response will serialized using a json serializer.
- An other Accept type can be 'application/xml' and in this case a Xml serializer will be used.
- 'application/msgpack' can also be handled automatically.
Streaming
For basic streaming response your action must return an object derived from a Stream. The firefly host will consume the stream and dispose it when finished. (Response streaming is one-way to the client)
[Action(Methods.GET)]
public Stream StreamingSample()
{
return new MemoryStream(Encoding.UTF8.GetBytes("This is a stream!"));
}
File Streaming
For a file streaming response your action can either return a StreamWithMetadata or a FileStreamResult object.
The firefly host will consume the stream and dispose it when finished. (Response streaming is one-way to the client)
using FileStreamResult :
[Action(Methods.GET)]
public FileStreamResult FileDownloadSample()
{
var filePath = Path.Combine("Content", "TextFile.txt");
return new FileStreamResult(filePath, "text/plain", "SampleTextFile.txt");
}
using StreamWithMetadata :
[Action(Methods.GET)]
public StreamWithMetadata FileDownloadSample()
{
var stream = new FileStream(Path.Combine("Content", "TextFile.txt"), FileMode.Open, FileAccess.Read, FileShare.Read);
return new StreamWithMetadata(stream, "text/plain").AsAttachment(fileName: "SampleTextFile.txt");
}
IAsyncEnumerable<T>
You can asynchronously yield results back to caller from your Action using the IAsyncEnumerable<T> type, like so:
[Action(Methods.GET)]
public async IAsyncEnumerable<string> ResultStreamingSample()
{
for (int n = 0; n < 10; n++)
{
yield return $"value is " + n + Environment.NewLine;
//simulate processing or IO operations
await Task.Delay(1000);
}
}