I'm using servicestack to build a web api serving some old data over the web. Unfortunately the data schema does not lend itself particularly well to the standard use of ServiceStack.Ormlite. For example there are no primary keys and on one table the (non-unique) key actually can contain any characters. In my sample database there are '/' characters inside the key.
Therefore when requesting a resource from the web api using this route "/api/objects/{objectCode}" if the required objectCode is 1/1 the route should be "/api/objects/1/1" but this results in a page not found exception. And what about when I request a resource on the following route "/api/objects/{objectCode}/subObjects"
does anyone know how I should be working around this problem? Should I be designing around the problem or is there something I can do to allow it?
Also, ideally I'd like to be able to pass an array of these objectCodes but I cannot guarantee that there won't be a ,
in the objectCode values so the delimiting character would appear in the resource code and thus ServiceStack's delimiter parsing, no?
I have searched for clues already but have only found people asking about encoding in the query string rather than the URI itself.
Unfortunately using forward slashes, commas and other unusual characters in the URL will produce undesired results and obviously isn't easy to work with in your routes. But it is possible to work around this by encoding the Id values.
We can use ServiceStack's great flexibility of filters to make encoding and decoding of the values completely transparent to your existing ServiceStack Service.
Transparent Encoding/Decoding Complex Id value
This method will use a Request and Response filter attribute:
- The Request filter will be responsible for decoding any encoded Id values.
- The Response filter will be responsible for encoding the plain Id value.
So the Id value will always be encoded in transit, and completely decoded in your Server-side Service implementation.
Full Source Code Here
In my example I have used base64 encoding. But you can substitute this for any encoding you want. For example you may choose to simply convert forward slashes to an underscore.
The attribute that does the encoding and decoding:
public class UsesEncodedAttribute : Attribute, IHasRequestFilter, IHasResponseFilter
{
IHasRequestFilter IHasRequestFilter.Copy()
{
return this;
}
IHasResponseFilter IHasResponseFilter.Copy()
{
return this;
}
public void RequestFilter(IRequest req, IResponse res, object requestDto)
{
// Decode the properties on the requestDto having the EncodedId attribute
var type = requestDto.GetType();
var properties = type.GetPublicProperties();
foreach(var p in properties)
{
// Find the property marked with EncodedId that is of type string, that can be read and written to
if(!p.HasAttribute<EncodedIdAttribute>() || p.PropertyType != typeof(string) || !p.CanRead || !p.CanWrite)
continue;
// Get the encoded value
string encodedValue = p.GetValue(requestDto, null) as string;
if(encodedValue != null)
{
// Decode the value from base64
string decodedValue = Encoding.UTF8.GetString(Convert.FromBase64String(encodedValue));
// Set the value to decoded string
p.SetValue(requestDto, decodedValue, null);
}
}
}
public void ResponseFilter(IRequest req, IResponse res, object response)
{
// Encode properties on the response having the EncodedId attribute
var type = response.GetType();
var properties = type.GetPublicProperties();
foreach(var p in properties)
{
// Find the property marked with EncodedId that is of type string, that can be read and written to
if(!p.HasAttribute<EncodedIdAttribute>() || p.PropertyType != typeof(string) || !p.CanRead || !p.CanWrite)
continue;
// Get the decoded value
string decodedValue = p.GetValue(response, null) as string;
if(decodedValue != null)
{
// Encode the value to base64
string encodedValue = Convert.ToBase64String(decodedValue.ToUtf8Bytes());
// Set the value to decoded string
p.SetValue(response, encodedValue, null);
}
}
}
// The lowest priority means it will run first, before your other filters
public int Priority { get { return int.MinValue; } }
}
A simple attribute to mark the properties that need Encoding/Decoding:
public class EncodedIdAttribute : Attribute { }
Usage:
Simply add a [UsesEncodedAttribute]
attribute to your request and response DTOs that have an encoded Id value. Then mark the properties that require encoding/decoding with the [EncodedId]
attribute. Note you can mark multiple properties with this attribute, useful if you have foreign keys.
[UsesEncodedAttribute]
[Route("/Object/{Id}","GET")]
public class GetObjectWithComplexIdRequest : IReturn<ObjectWithComplexIdResponse>
{
[EncodedId]
public string Id { get; set; }
}
[UsesEncodedAttribute]
public class ObjectWithComplexIdResponse
{
[EncodedId]
public string Id { get; set; }
}
public class ComplexIdTestService : Service
{
public ObjectWithComplexIdResponse Get(GetObjectWithComplexIdRequest request)
{
Console.WriteLine("The requested id is {0}", request.Id);
return new ObjectWithComplexIdResponse { Id = request.Id };
}
}
Output:
When we navigate to localhost:8081/Object/SGVsbG8vV29ybGQsVGVzdA==
we see in the console that accessing the Id
property on our request DTO yields the original decoded Id value of Hello/World,Test
. It is notable that the plain Id that we add to the response, is automatically encoded in the reply.
Had we returned:
return new ObjectWithComplexIdResponse { Id = "Another/Complex/Id:Test" }
then the response to the client would have been
{ "Id": "QW5vdGhlci9Db21wbGV4L0lkOlRlc3Q=" }
Edit - Added support for array of Ids:
Use the linked code below to allow collections of encoded Ids to be sent or received.
Full Source Code - With support for array of Ids
For example: http://localhost:8081/Object/SGVsbG8vU29tZXRoaW5nL0Nvb2w=,VGhpcy9Jcy9BLVRlc3Q6SWQ=
[UsesEncodedAttribute]
[Route("/Object/{Ids}","GET")]
public class GetObjectWithComplexIdRequest : IReturn<ObjectWithComplexIdResponse>
{
[EncodedId]
public string[] Ids { get; set; }
}
[UsesEncodedAttribute]
public class ObjectWithComplexIdResponse
{
[EncodedId]
public string[] Ids { get; set; }
}