1.简介

refit是一个受到square的retrofit库(java)启发的自动类型安全rest库。通过httpclient网络请求(post,get,put,delete等封装)把rest api返回的数据转化为poco(plain ordinary c# object,简单c#对象) to json。我们的应用程序通过refit请求网络,实际上是使用refit接口层封装请求参数、header、url等信息,之后由httpclient完成后续的请求操作,在服务端返回数据之后,httpclient将原始的结果交给refit,后者根据用户的需求对结果进行解析的过程。安装组件命令行:

install-package refit

代码例子:

[headers("user-agent: refit integration tests")]//这里因为目标源是githubapi,所以一定要加入这个静态请求标头信息,让其这是一个测试请求,不然会返回数据异常。
public interface igithubapi
{
    [get("/users/{user}")]
    task<user> getuser(string user);
}
public class githubapi
{
    public async task<user> getuser()
    {
        var githubapi = restservice.for<igithubapi>("https://api.github.com");
        var octocat = await githubapi.getuser("octocat");
        return octocat;
    }
}
public class user
{
    public string login { get; set; }
    public int? id { get; set; }
    public string url { get; set; }
}
[httpget]
public async task<actionresult<ienumerable<string>>> get()
{
    var result = await new githubapi().getuser();
    return new string[] { result.id.value.tostring(), result.login };
}

注:接口中headers、get这些属性叫做refit的特性。
定义上面的一个igithubapi的rest api接口,该接口定义了一个函数getuser,该函数会通过http get请求去访问服务器的/users/{user}路径把返回的结果封装为user poco对象并返回。其中url路径中的{user}的值为getuser函数中的参数user的取值,这里赋值为octocat。然后通过restservice类来生成一个igithubapi接口的实现并供httpclient调用。

 

 

2.api属性

每个方法必须具有提供请求url和http属性。http属性有六个内置注释:get, post, put, delete, patch and head,例:

[get("/users/list")]

您还可以在请求url中指定查询参数:

[get("/users/list?sort=desc")]

还可以使用相对url上的替换块和参数来动态请求资源。替换块是由{and,即&}包围的字母数字字符串。如果参数名称与url路径中的名称不匹配,请使用aliasas属性,例:

[get("/group/{id}/users")]
task<list<user>> grouplist([aliasas("id")] int groupid);

请求url还可以将替换块绑定到自定义对象,例:

[get("/group/{request.groupid}/users/{request.userid}")]
task<list<user>> grouplist(usergrouprequest request);
class usergrouprequest{
    int groupid { get;set; }
    int userid { get;set; }
}

未指定为url替换的参数将自动用作查询参数。这与retrofit不同,在retrofit中,必须明确指定所有参数,例:

[get("/group/{id}/users")]
task<list<user>> grouplist([aliasas("id")] int groupid, [aliasas("sort")] string sortorder);
grouplist(4, "desc");

输出结果:”/group/4/users?sort=desc”

3.动态查询字符串参数(dynamic querystring parameters)

方法还可以传递自定义对象,把对象属性追加到查询字符串参数当中,例如:

public class myqueryparams
{
    [aliasas("order")]
    public string sortorder { get; set; }
    public int limit { get; set; }
}
[get("/group/{id}/users")]
task<list<user>> grouplist([aliasas("id")] int groupid, myqueryparams params);
[get("/group/{id}/users")]
task<list<user>> grouplistwithattribute([aliasas("id")] int groupid, [query(".","search")]myqueryparams params);
params.sortorder = "desc";
params.limit = 10;
grouplist(4, params)

输出结果:”/group/4/users?order=desc&limit=10″

grouplistwithattribute(4, params)

输出结果:”/group/4/users?search.order=desc&search.limit=10″
您还可以使用[query]指定querystring参数,并将其在非get请求中扁平化,类似于:

[post("/statuses/update.json")]
task<tweet> posttweet([query]tweetparams params);

5.集合作为查询字符串参数(collections as querystring parameters)

方法除了支持传递自定义对象查询,还支持集合查询的,例:

[get("/users/list")]
task search([query(collectionformat.multi)]int[] ages);
search(new [] {10, 20, 30})

输出结果:”/users/list?ages=10&ages=20&ages=30″

[get("/users/list")]
task search([query(collectionformat.csv)]int[] ages);
search(new [] {10, 20, 30})

输出结果:”/users/list?ages=10%2c20%2c30″

6.转义符查询字符串参数(unescape querystring parameters)

使用queryuriformat属性指定查询参数是否应转义网址,例:

[get("/query")]
[queryuriformat(uriformat.unescaped)]
task query(string q);
query("select+id,name+from+account")

输出结果:”/query?q=select+id,name+from+account”

7.body内容

通过使用body属性,可以把自定义对象参数追加到http请求body当中。

[post("/users/new")]
task createuser([body] user user)

根据参数的类型,提供body数据有四种可能性:
●如果类型为stream,则内容将通过streamcontent流形式传输。
●如果类型为string,则字符串将直接用作内容,除非[body(bodyserializationmethod.json)]设置了字符串,否则将其作为stringcontent。
●如果参数具有属性[body(bodyserializationmethod.urlencoded)],则内容将被url编码。
●对于所有其他类型,将使用refitsettings中指定的内容序列化程序将对象序列化(默认为json)。
●缓冲和content-length头
默认情况下,refit重新调整流式传输正文内容而不缓冲它。例如,这意味着您可以从磁盘流式传输文件,而不会产生将整个文件加载到内存中的开销。这样做的缺点是没有在请求上设置内容长度头(content-length)。如果您的api需要您随请求发送一个内容长度头,您可以通过将[body]属性的缓冲参数设置为true来禁用此流行为:

task createuser([body(buffered: true)] user user);

7.1.json内容

使用json.net对json请求和响应进行序列化/反序列化。默认情况下,refit将使用可以通过设置newtonsoft.json.jsonconvert.defaultsettings进行配置的序列化器设置:

jsonconvert.defaultsettings =
    () => new jsonserializersettings() {
        contractresolver = new camelcasepropertynamescontractresolver(),
        converters = {new stringenumconverter()}
    };
//serialized as: {"day":"saturday"}
await postsomestuff(new { day = dayofweek.saturday });

由于默认静态配置是全局设置,它们将影响您的整个应用程序。有时候我们只想要对某些特定api进行设置,您可以选择使用refitsettings属性,以允许您指定所需的序列化程序进行设置,这使您可以为单独的api设置不同的序列化程序设置:

var githubapi = restservice.for<igithubapi>("https://api.github.com",
    new refitsettings {
        contentserializer = new jsoncontentserializer(
            new jsonserializersettings {
                contractresolver = new snakecasepropertynamescontractresolver()
        }
    )});
var otherapi = restservice.for<iotherapi>("https://api.example.com",
    new refitsettings {
        contentserializer = new jsoncontentserializer(
            new jsonserializersettings {
                contractresolver = new camelcasepropertynamescontractresolver()
        }
    )});

还可以使用json.net的jsonproperty属性来自定义属性序列化/反序列化:

public class foo
{
    //像[aliasas(“ b”)]一样会在表单中发布
    [jsonproperty(propertyname="b")]
    public string bar { get; set; }
} 

7.2xml内容

xml请求和响应使用system.xml.serialization.xmlserializer进行序列化/反序列化。默认情况下,refit只会使用json将内容序列化,若要使用xml内容,请将contentserializer配置为使用xmlcontentserializer:

var githubapi = restservice.for<ixmlapi>("https://www.w3.org/xml",
    new refitsettings {
        contentserializer = new xmlcontentserializer()
});

属性序列化/反序列化可以使用system.xml.serialization命名空间中的属性进行自定义:

public class foo
{
   [xmlelement(namespace = "https://www.w3.org/xml")]
   public string bar { get; set; }
}

system.xml.serialization.xmlserializer提供了许多序列化选项,可以通过向xmlcontentserializer构造函数提供xmlcontentserializer设置来设置这些选项:

var githubapi = restservice.for<ixmlapi>("https://www.w3.org/xml",
    new refitsettings {
        contentserializer = new xmlcontentserializer(
            new xmlcontentserializersettings
            {
                xmlreaderwritersettings = new xmlreaderwritersettings()
                {
                    readersettings = new xmlreadersettings
                    {
                        ignorewhitespace = true
                    }
                }
            }
        )
});

7.3.表单发布(form posts)

对于以表单形式发布(即序列化为application/x-www-form-urlencoded)的api,请使用初始化body属性bodyserializationmethod.urlencoded属性,参数可以是idictionary字典,例:

public interface imeasurementprotocolapi
{
    [post("/collect")]
    task collect([body(bodyserializationmethod.urlencoded)] dictionary<string, object> data);
}
var data = new dictionary<string, object> {
    {"v", 1},
    {"tid", "ua-1234-5"},
    {"cid", new guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c")},
    {"t", "event"},
};
// serialized as: v=1&tid=ua-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
await api.collect(data);

如果我们传递对象跟请求表单中字段名称不一致时,可在对象属性名称上加入[aliasas(“你定义字段名称”)] 属性,那么加入属性的对象字段都将会被序列化为请求中的表单字段:

public interface imeasurementprotocolapi
{
    [post("/collect")]
    task collect([body(bodyserializationmethod.urlencoded)] measurement measurement);
}
public class measurement
{
    // properties can be read-only and [aliasas] isn't required
    public int v { get { return 1; } }
    [aliasas("tid")]
    public string webpropertyid { get; set; }
    [aliasas("cid")]
    public guid clientid { get; set; }
    [aliasas("t")]
    public string type { get; set; }
    public object ignoreme { private get; set; }
}
var measurement = new measurement {
    webpropertyid = "ua-1234-5",
    clientid = new guid("d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c"),
    type = "event"
};
// serialized as: v=1&tid=ua-1234-5&cid=d1e9ea6b-2e8b-4699-93e0-0bcbd26c206c&t=event
await api.collect(measurement);

8.设置请求头

8.1静态头(static headers)

您可以为将headers属性应用于方法的请求设置一个或多个静态请求头:

[headers("user-agent: awesome octocat app")]
[get("/users/{user}")]
task<user> getuser(string user);

通过将headers属性应用于接口,还可以将静态头添加到api中的每个请求:

[headers("user-agent: awesome octocat app")]
public interface igithubapi
{
    [get("/users/{user}")]
    task<user> getuser(string user);
    [post("/users/new")]
    task createuser([body] user user);
}

8.2动态头(dynamic headers)

如果需要在运行时设置头的内容,则可以通过将头属性应用于参数来向请求添加具有动态值的头:

[get("/users/{user}")]
task<user> getuser(string user, [header("authorization")] string authorization);
// will add the header "authorization: token oauth-token" to the request
var user = await getuser("octocat", "token oauth-token"); 

8.3授权(动态头redux)

使用头的最常见原因是为了授权。而现在大多数api使用一些oauth风格的访问令牌,这些访问令牌会过期,刷新寿命更长的令牌。封装这些类型的令牌使用的一种方法是,可以插入自定义的httpclienthandler。这样做有两个类:一个是authenticatedhttpclienthandler,它接受一个func<task<string>>参数,在这个参数中可以生成签名,而不必知道请求。另一个是authenticatedparameteredhttpclienthandler,它接受一个func<httprequestmessage,task<string>>参数,其中签名需要有关请求的信息(参见前面关于twitter的api的注释),
例如:

class authenticatedhttpclienthandler : httpclienthandler
{
    private readonly func<task<string>> gettoken;
    public authenticatedhttpclienthandler(func<task<string>> gettoken)
    {
        if (gettoken == null) throw new argumentnullexception(nameof(gettoken));
        this.gettoken = gettoken;
    }
    protected override async task<httpresponsemessage> sendasync(httprequestmessage request, cancellationtoken cancellationtoken)
    {
        // see if the request has an authorize header
        var auth = request.headers.authorization;
        if (auth != null)
        {
            var token = await gettoken().configureawait(false);
            request.headers.authorization = new authenticationheadervalue(auth.scheme, token);
        }
        return await base.sendasync(request, cancellationtoken).configureawait(false);
    }
}

或者:

class authenticatedparameterizedhttpclienthandler : delegatinghandler
    {
        readonly func<httprequestmessage, task<string>> gettoken;
        public authenticatedparameterizedhttpclienthandler(func<httprequestmessage, task<string>> gettoken, httpmessagehandler innerhandler = null)
            : base(innerhandler ?? new httpclienthandler())
        {
            this.gettoken = gettoken ?? throw new argumentnullexception(nameof(gettoken));
        }

        protected override async task<httpresponsemessage> sendasync(httprequestmessage request, cancellationtoken cancellationtoken)
        {
            // see if the request has an authorize header
            var auth = request.headers.authorization;
            if (auth != null)
            {
                var token = await gettoken(request).configureawait(false);
                request.headers.authorization = new authenticationheadervalue(auth.scheme, token);
            }
            return await base.sendasync(request, cancellationtoken).configureawait(false);
        }
    }

虽然httpclient包含一个几乎相同的方法签名,但使用方式不同。重新安装未调用httpclient.sendasync。必须改为修改httpclienthandler。此类的用法与此类似(示例使用adal库来管理自动令牌刷新,但主体用于xamarin.auth或任何其他库:

class loginviewmodel
{
    authenticationcontext context = new authenticationcontext(...);
    private async task<string> gettoken()
    {
        // the acquiretokenasync call will prompt with a ui if necessary
        // or otherwise silently use a refresh token to return
        // a valid access token    
        var token = await context.acquiretokenasync("http://my.service.uri/app", "clientid", new uri("callback://complete"));
        return token;
    }
    public async task loginandcallapi()
    {
        var api = restservice.for<imyrestservice>(new httpclient(new authenticatedhttpclienthandler(gettoken)) { baseaddress = new uri("https://the.end.point/") });
        var location = await api.getlocationofrebelbase();
    }
}
interface imyrestservice
{
    [get("/getpublicinfo")]
    task<foobar> somepublicmethod();
    [get("/secretstuff")]
    [headers("authorization: bearer")]
    task<location> getlocationofrebelbase();
}

在上面的示例中,每当调用需要身份验证的方法时,authenticatedhttpclienthandler将尝试获取新的访问令牌。由应用程序提供,检查现有访问令牌的过期时间,并在需要时获取新的访问令牌。

参考文献:
refit