欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

(27)ASP.NET Core .NET标准REST库Refit

程序员文章站 2022-08-05 08:14:00
1.简介 Refit是一个受到Square的Retrofit库(Java)启发的自动类型安全REST库。通过HttpClient网络请求(POST,GET,PUT,DELETE等封装)把REST API返回的数据转化为POCO(Plain Ordinary C# Object,简单C#对象) to ......

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调用。

(27)ASP.NET Core .NET标准REST库Refit

 

 

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