Static Typescript Client



3 Steps to generate static typescript client



Michał Niegrzybowski - @mnie8

What we need

Concrete return types

1: 
2: 
3: 
4: 
5: 
public JsonResult Save(int userId)
{
    var r = _someService.DoSomething(userId);
    return new JsonResult(r, JsonResult.AllowGet);
}

Concrete parameters types

1: 
2: 
3: 
4: 
5: 
6: 
public JsonResult GetTable(string data)
{
    var d = JsonConvert.DeserializeObject<TableParameters>(data);
    var r = _someService.DoSomething(d);
    return new JsonResult(r, JsonResult.AllowGet);
}

Some tool to do it automatically

Step 1

Remove all explicit deserialization

1: 
2: 
3: 
4: 
5: 
6: 
public JsonResult GetTable(string data)
{
    var d = JsonConvert.DeserializeObject<TableParameters>(data);
    var r = _someService.DoSomething(d);
    return new JsonResult(r, JsonResult.AllowGet);
}

Custom binder

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
internal class CustomModelBinder : IModelBinder
{
    private readonly Type _type;
    public CustomModelBinder(Type @type)
    {
        _type = @type;
    }

    public object BindModel(ControllerContext c, ModelBindingContext mb)
    {
        var json = controllerContext
            .HttpContext
            .Request
            .Params
            .Get(mb.ModelName);

        return json == null
            ? null
            : JsonConvert.DeserializeObject(json, _type);
    }
}

Custom binder

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
private static readonly IReadOnlyCollection<Type> CustomBinderTypes = 
new[] {
    typeof(TypeA),
    typeof(TypeB)
};

protected void Application_Start()
{
    …
    CustomBinderTypes
        .ForEach(t => ModelBinders
            .Binders
            .Add(t, new CustomModelBinder(t))
        );
    …
}

Final result

From:

1: 
2: 
3: 
4: 
5: 
6: 
public JsonResult GetTable(string data)
{
    var d = JsonConvert.DeserializeObject<TableParameters>(data);
    var r = _someService.DoSomething(d);
    return new JsonResult(r, JsonResult.AllowGet);
}

To:

1: 
2: 
3: 
4: 
5: 
public JsonResult GetTable(TableParameters data)
{
    var r = _someService.DoSomething(data);
    return new JsonResult(r, JsonResult.AllowGet);
}

Step 2

Return concrete types instead of Json/ActionResult

1: 
2: 
3: 
4: 
5: 
public JsonResult Save(int userId)
{
    var r = _someService.DoSomething(userId);
    return new JsonResult(r, JsonResult.AllowGet);
}

Use Controller Action Invoker

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
public class WebApiInvoker : AsyncControllerActionInvoker
{
    protected override ActionResult CreateActionResult(
        ControllerContext c,
        ActionDescriptor a,
        object value
    )
    {
        var @type = (a as ReflectedActionDescriptor)
            ?.MethodInfo
            ?.ReturnType;

        return typeof(ActionResult).IsAssignableFrom(@type)
            ? base.CreateActionResult(c, a, value)
            : new JsonResult(){data = value …};
    }
}

What if we don't check return type?

How to use it? #1

1: 
2: 
3: 
4: 
5: 
6: 
7: 
public SomeController : Controller
{
    public SomeController()
    {
        ActionInvoker = new WebApiInvoker();
    }
}

How to use it? #2

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
internal class ServiceLocatorControllerFactory
    : DefaultControllerFactory
{
    protected override IController GetControllerInstance(
        RequestContext r,
        Type t
    )
    {
        var con = (Controller) ServiceLocator.Current.GetInstance(t);
        con.ActionInvoker = new WebApiInvoker();
        return con;
    }
}

Step 3

Use TypeWriter

Basic Template

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
${
    using Typewriter.Extensions.WebApi;
}

import { CallService } from "../core/core.module";
module App { $Classes(*Controller)[
    export class $Name {
        constructor(private http: CallService) {
        } $Methods[
        
        public $name = ($Parameters[$name: $Type][, ]) => {
            return this.http.$HttpMethod(`$Url`, $RequestData);
        }]
    }]
}

Final solution

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { CallService } from "../core/core.module";
$Classes($ShouldScanController)[$ImportStatements

@Injectable()
export class $Name {
    constructor(private callService: CallService) {
    } $Methods[
        
    public $MethodName = ($Parameters[$name: $Type][, ]): Observable<$Type> => {
        return this.callService.$MethodType(`$Url`, $RequestData);
    }]
}]

Final solution

Show template in Visual Studio.

Example of generated contract

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
import { Injectable } from "@angular/core";
import { Observable } from "rxjs";
import { CallService } from "../core/core.module";
import { Result } from "../contract/Result";


@Injectable()
export class ExportRefreshController {
    constructor(private callService: CallService) {
    } 
        
    public html = (queryId: number): Observable<string> => {
        return this.callService.post(`v1/excel/html?queryId=${queryId}`, null);
    }
}

Summary

Pros

  • no more hardcoded paths
  • if controller changes, contract also changes
  • no more mistakes in types of call post/get
  • you want to call FilterController, you inject FilterController

Cons

Thanks