ServiceStack/ServiceStack — pull request #611 by julrichkieffer (2013-04-19 14:39:41+00:00)
Title: Extensible route inference

The new API introduces a **IServiceRoutes.AddFromAssembly()** extension method. This is fantastic as it opens the door to routes-by-convention by "detecting" routes via the combination of Service/Method's name and the RequestDTO. Sadly, **the implementation is limited to the DTO's type name and (optionally) the presence of a case-sensitive Id member**.

I had to pull the SS source to find out why my custom DTO members were largely ignored until I had the (magic string) Id member -- the limitation described above.

And once found, I set out to create the extensibility I thought SS needed...
* I introduced an alternative inference mechanism, one that called to a new list of delegates instead of hard-coding the "detection"
* I created a new RouteInferenceStrategies static class that holds the delegate implementations for the "built-in" conventions:

1. The RequestDTO's type name
2. The presence of any public property with **names matching a List**. (ie "Id", "IDs")
3. The presence of any public property marked with a **matching attribute**. (ie [PrimaryKey])

Both 2 and 3 are public, allowing the inclusion of domain-specific fiels / attributes to be inferred as route members. More so, the list of delegates itself allows from domain-specific inference strategies to be used.

* In keeping with the "reasonable defaults" design principal, I attempted to specify the default "UseTheDTO'sTypeName" and "UseTheIdMember" delegates when *ServiceStack.WebHost.Endpoints.EndpointHostConfig* is instantiated, but this would create circular references on ServiceStack.dll. So instead, I reasoned that if no strategies are defined by the time AddFromAssembly() is called, then the "built-in" delegates should be used... a sort of late-bound reasonable defaults :)

I would like the above to be rolled into the master branch of SS. But I've created this pull request so that focussed review and other quality requirements can be met first.

Please note that I'm a novice to SS, so much so that I couldn't get the test projects to compile let alone execute unit tests! More so, having just read a book, I was able to learn enough about Git to fork SS, create this branch, and now this pull request. But I'm willing to learn on-the-job as it were :)

Here's an example implementation:

AppHost.cs:

```csharp
public override void Configure(Funq.Container container) {
//Set JSON web services to return idiomatic JSON camelCase properties
ServiceStack.Text.JsConfig.EmitCamelCaseNames = true;

//Supplement default inference strategies with domain-specific markers
RouteInferenceStrategies.AttributesToMatch.Add(typeof(KeyAttribute));
RouteInferenceStrategies.AttributesToMatch.Add(typeof(RequiredAttribute));
RouteInferenceStrategies.PropertyNamesToMatch.Add("Key");

//Configure REST paths via route detection
Routes.AddFromAssembly(typeof(AppHost).Assembly);

//Set MVC to use the same Funq IOC as ServiceStack
ControllerBuilder.Current.SetControllerFactory(new FunqControllerFactory(container));
}
```

RoutesInferenceService.cs:

```csharp
using System;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.Contracts;
using ServiceStack.Common;
using ServiceStack.DataAnnotations;
using ServiceStack.ServiceInterface;
using ServiceStack.Text;

namespace SSTester.Services {

public class RoutesInferenceService : Service {

public object Get(RequestNoMembers request) {
return request.AsTypeString();
}

public object Get(RequestWithMemberCalledId request) {
return request.AsTypeString();
}

public object Get(RequestWithPrimaryKeyAttribute request) {
return request.AsTypeString();
}

public object Get(RequestWithArrayMemberCalledId request) {
return request.AsTypeString();
}

public object Get(RequestWithArrayMemberCalledIds request) {
return request.AsTypeString();
}

public object Get(RequestWithMemberWithRequiredAttribute request) {
return request.AsTypeString();
}

public object Get(RequestWithMemberWithKeyAttribute request) {
return request.AsTypeString();
}

public object Get(RequestWithCompositeKeys request) {
return request.AsTypeString();
}

public object Get(RequestWithMultipleSpecialNames request) {
return request.AsTypeString();
}
}

public static class ObjectExtensions {

public static string AsTypeString(this object request) {
Contract.Requires(request != null);
Contract.Ensures(!Contract.Result().IsNullOrEmpty());

return request.GetType().ToTypeString();
}

}

public class RequestNoMembers { }

public class RequestWithMemberCalledId {
public object Id { get; set; }
}

public class RequestWithPrimaryKeyAttribute {
[PrimaryKey]
public int PrimaryKey { get; set; }
}

public class RequestWithArrayMemberCalledId {
public object[] Id { get; set; }
}

public class RequestWithArrayMemberCalledIds {
public object[] Ids { get; set; }
}

public class RequestWithMemberWithRequiredAttribute {
[Required]
public int Required { get; set; }
}

public class RequestWithMemberWithKeyAttribute {
[Key]
public string Key { get; set; }
}

public class RequestWithCompositeKeys {
[PrimaryKey]
public int PK1 { get; set; }

[PrimaryKey]
public int PK2 { get; set; }

[Key]
public int K1 { get; set; }

[Key]
public int K2 { get; set; }

[Required]
public int R1 { get; set; }

[Required]
public int R2 { get; set; }
}

public class RequestWithMultipleSpecialNames {
public int Id { get; set; }
public int[] Ids { get; set; }
public string Key { get; set; }
}

}
```