Refit.Newtonsoft.Json 7.1.2

前缀已预留

需要NuGet 2.12或更高版本。

dotnet add package Refit.Newtonsoft.Json --version 7.1.2                
NuGet\Install-Package Refit.Newtonsoft.Json -Version 7.1.2                
此命令旨在在Visual Studio中的包管理器控制台中使用,因为它使用了NuGet模块版本的Install-Package
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.1.2" />                
对于支持包引用的项目,将此XML节点复制到项目文件中以引用包。
paket add Refit.Newtonsoft.Json --version 7.1.2                
#r "nuget: Refit.Newtonsoft.Json, 7.1.2"                
#r指令可以在F# Interactive和Polyglot Notebooks中用于。将此内容复制到交互式工具或脚本的源代码中,以引用该包。
// Install Refit.Newtonsoft.Json as a Cake Addin
#addin nuget:?package=Refit.Newtonsoft.Json&version=7.1.2

// Install Refit.Newtonsoft.Json as a Cake Tool
#tool nuget:?package=Refit.Newtonsoft.Json&version=7.1.2                

Refit

Refit:适用于.NET Core、Xamarin和.NET的自动类型安全的REST库

Build codecov

Refit Refit.HttpClientFactory Refit.Newtonsoft.Json
NuGet NuGet NuGet NuGet

Refit 是一个受到 Square 的 Retrofit 库强烈启发的库,它可以将你的 REST API 转换为实时接口。

public interface IGitHubApi
{
    [Get("/users/{user}")]
    Task<User> GetUser(string user);
}

RestService 类生成一个 IGitHubApi 的实现,该实现使用 HttpClient 来执行调用。

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com");
var octocat = await gitHubApi.GetUser("octocat");

.NET Core 支持通过 HttpClientFactory 进行注册。

services
    .AddRefitClient<IGitHubApi>()
    .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.github.com"));

目录

这个功能在哪里工作?

Refit 当前支持的以下平台和任何 .NET Standard 2.0 目标

  • UWP
  • Xamarin.Android
  • Xamarin.Mac
  • Xamarin.iOS
  • 桌面 .NET 4.6.1
  • .NET 5 / .NET Core
  • Blazor
  • Uno Platform

SDK 要求

Refit 6 需要 Visual Studio 16.8 或更高版本,或者 .NET SDK 5.0.100 或更高版本。它可以针对任何 .NET Standard 2.0 平台。

Refit 6 不支持旧 packages.config 格式的 NuGet 引用(因为它们不支持分析器/源生成器)。你必须迁移到 PackageReference 才能使用 Refit v6 及更高版本。

6.x 版本中的重大更改

Refit 6 使得 System.Text.Json 成为默认的 JSON 序列化器。如果你希望继续使用 Newtonsoft.Json,请添加 Refit.Newtonsoft.Json NuGet 包,并将你的 RefitSettings 实例上的 ContentSerializer 设置为 NewtonsoftJsonContentSerializerSystem.Text.Json 更快且内存使用更少,尽管并非所有功能都得到支持。有关更多详细信息,请参阅迁移指南

IContentSerializer 已重命名为 IHttpContentSerializer,以更好地反映其目的。此外,其中的两个方法也被重命名,SerializeAsync<T>ToHttpContent<T>DeserializeAsync<T>FromHttpContentAsync<T>。任何现有的实现都需要更新,尽管这些更改应该很小。

6.3 中的更新

Refit 6.3 将通过 XmlContentSerializer 的 XML 序列化分离到单独的包 Refit.Xml 中。这样做是为了在使用 Refit 与 Web Assembly (WASM) 应用程序时减少依赖项大小。如果你需要 XML,请添加对 Refit.Xml 的引用。

API 属性

每个方法都必须有一个 HTTP 属性,该属性提供请求方法和相对 URL。有六个内置注解:Get、Post、Put、Delete、Patch 和 Head。资源的相对 URL 由注解指定。

[Get("/users/list")]

你还可以在 URL 中指定查询参数。

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

可以通过替换块和方法参数动态更新请求URL。替换块是由大括号{}包围的字符字符串。

如果您的参数名称与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中,所有参数都必须显式指定。

参数名称与URL参数之间的比较不区分大小写,因此如果您将参数命名为groupId放置在路径/group/{groupid}/show中,示例,它将正常工作。

[Get("/group/{id}/users")]
Task<List<User>> GroupList([AliasAs("id")] int groupId, [AliasAs("sort")] string sortOrder);

GroupList(4, "desc");
>>> "/group/4/users?sort=desc"

往返路由参数语法:当使用双星号(*)通配符参数语法时,正斜杠不会被编码。

在生成链接时,路由系统会编码双星号(*)通配符参数(例如,{**myparametername})中捕获的值(除了正斜杠)。

往返路由参数的类型必须是字符串。

[Get("/search/{**page}")]
Task<List<Page>> Search(string page);

Search("admin/products");
>>> "/search/admin/products"

查询字符串

动态查询字符串参数

如果您指定了一个object作为查询参数,则所有非null的公开属性都用作查询参数。这之前仅应用于GET请求,但现在已扩展到所有HTTP请求方法,部分原因是Twitter的混合API坚持使用带有查询字符串参数的非GET请求。使用Query属性将行为更改为'扁平化'查询参数对象。如果使用此属性,您可以指定分隔符和前缀的值,它们用于'扁平化'对象。

public class MyQueryParams
{
    [AliasAs("order")]
    public string SortOrder { get; set; }

    public int Limit { get; set; }

    public KindOptions Kind { get; set; }
}

public enum KindOptions
{
    Foo,

    [EnumMember(Value = "bar")]
    Bar
}


[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;
params.Kind = KindOptions.Bar;

GroupList(4, params)
>>> "/group/4/users?order=desc&Limit=10&Kind=bar"

GroupListWithAttribute(4, params)
>>> "/group/4/users?search.order=desc&search.Limit=10&search.Kind=bar"

如果使用字典,也存在类似的行为,但没有AliasAs属性的优点,当然也没有智能提示和/或类型安全。

您还可以使用[Query]指定查询字符串参数,并在非GET请求中将其扁平化,类似于

[Post("/statuses/update.json")]
Task<Tweet> PostTweet([Query]TweetParams params);

其中TweetParams是一个POCO,属性也将支持[AliasAs]属性。

将集合作为查询字符串参数

使用Query属性指定集合在查询字符串中的格式。

[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"

您还可以在RefitSettings中指定集合格式,将默认使用,除非在Query属性中显式定义。

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        CollectionFormat = CollectionFormat.Multi
    });
转义查询字符串参数

使用QueryUriFormat属性指定是否对查询参数进行URL编码。

[Get("/query")]
[QueryUriFormat(UriFormat.Unescaped)]
Task Query(string q);

Query("Select+Id,Name+From+Account")
>>> "/query?q=Select+Id,Name+From+Account"
自定义查询字符串参数格式化

格式化键

要自定义查询键的格式,您有两个主要选项

  1. 使用AliasAs属性:

    您可以使用AliasAs属性为属性指定一个自定义键名称。此属性将始终优于您指定的任何键格式化程序。

    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, [Query] MyQueryParams params);
    
    params.SortOrder = "desc";
    params.Limit = 10;
    
    GroupList(1, params);
    

    这将生成以下请求

    /group/1/users?order=desc&Limit=10
    
  2. 使用RefitSettings.UrlParameterKeyFormatter属性:

    默认情况下,Refit使用属性名称作为查询键,不进行任何额外格式化。如果您希望对所有查询键应用自定义格式,则可以使用UrlParameterKeyFormatter属性。请记住,如果属性具有AliasAs属性,它将始终被使用,而不管格式化程序如何。

    以下示例使用内置的CamelCaseUrlParameterKeyFormatter

    public class MyQueryParams
    {
        public string SortOrder { get; set; }
    
        [AliasAs("queryLimit")]
        public int Limit { get; set; }
    }
    
    [Get("/group/users")]
    Task<List<User>> GroupList([Query] MyQueryParams params);
    
    params.SortOrder = "desc";
    params.Limit = 10;
    

    请求将如下所示

    /group/users?sortOrder=desc&queryLimit=10
    

注意AliasAs属性始终具有最高优先级。如果存在属性和自定义键格式化程序,则使用AliasAs属性的值。

使用UrlParameterFormatter格式化URL参数值

在Refit中,RefitSettings中的UrlParameterFormatter属性允许您自定义URL中参数值的格式。这在您需要以特定方式格式化日期、数字或其他类型以确保与API的期望一致时非常有用。

使用UrlParameterFormatter:

将实现IUrlParameterFormatter接口的自定义格式化器分配给UrlParameterFormatter属性。

public class CustomDateUrlParameterFormatter : IUrlParameterFormatter
{
    public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
    {
        if (value is DateTime dt)
        {
            return dt.ToString("yyyyMMdd");
        }

        return value?.ToString();
    }
}

var settings = new RefitSettings
{
    UrlParameterFormatter = new CustomDateUrlParameterFormatter()
};

在此示例中,为日期值创建了一个自定义格式化器。每当遇到 DateTime 参数时,它将日期格式化为 yyyyMMdd

格式化字典键:

处理字典时,请注意键被当作值来处理。如果您需要为字典键进行自定义格式化,应同时使用 UrlParameterFormatter

例如,如果您有一个字典参数并且想要以特定方式格式化其键,您可以在自定义格式化器中处理该操作。

public class CustomDictionaryKeyFormatter : IUrlParameterFormatter
{
    public string? Format(object? value, ICustomAttributeProvider attributeProvider, Type type)
    {
        // Handle dictionary keys
        if (attributeProvider is PropertyInfo prop && prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
        {
            // Custom formatting logic for dictionary keys
            return value?.ToString().ToUpperInvariant();
        }

        return value?.ToString();
    }
}

var settings = new RefitSettings
{
    UrlParameterFormatter = new CustomDictionaryKeyFormatter()
};

在上面的示例中,字典键将被转换为大写。

正文内容

您可以使用 Body 属性将方法中的一个参数用作正文。

[Post("/users/new")]
Task CreateUser([Body] User user);

根据参数类型,提供正文数据有四种可能性。

  • 如果类型是 Stream,则内容将通过 StreamContent 进行流式传输。
  • 如果类型是 string,则字符串将直接用作内容,除非设置了 [Body(BodySerializationMethod.Json)],它将作为 StringContent 发送。
  • 如果参数具有 [Body(BodySerializationMethod.UrlEncoded)] 属性,则内容将被 URL 编码(如下面的表单提交中所述)。
  • 对于所有其他类型,将使用 RefitSettings 中指定的内容序列化器序列化对象(JSON 是默认值)。
缓存和 Content-Length 标头

默认情况下,Refit 会流式传输身体内容而不进行缓存。这意味着您可以流式传输来自磁盘的文件等,而不必将整个文件加载到内存中。但是,这的缺点是没有在请求上设置 Content-Length 标头。如果您的 API 需要您在请求中发送 Content-Length 标头,您可以通过将 [Body] 属性的 buffered 参数设置为 true 禁用此流式传输行为。

Task CreateUser([Body(buffered: true)] User user);
JSON 内容

使用 IHttpContentSerializer 接口的一个实例进行序列化和反序列化 JSON 请求和响应。Refit 提供了两种默认实现:SystemTextJsonContentSerializer(默认 JSON 序列化器)和 NewtonsoftJsonContentSerializer。第一个使用 System.Text.Json API,注重高性能和低内存使用,而后者使用已知的 Newtonsoft.Json 库,更灵活和可定制。您可以在以下链接中了解更多关于这两个序列化器以及两者之间主要差异的信息:点击这里

例如,以下是如何使用基于 Newtonsoft.Json 的序列化器创建新的 RefitSettings 实例(您还需要向 Refit.Newtonsoft.Json 添加 PackageReference

var settings = new RefitSettings(new NewtonsoftJsonContentSerializer());

如果您使用的是 Newtonsoft.Json API,您可以通过设置 Newtonsoft.Json.JsonConvert.DefaultSettings 属性来自定义它们的 behavtior。

JsonConvert.DefaultSettings =
    () => new JsonSerializerSettings() {
        ContractResolver = new CamelCasePropertyNamesContractResolver(),
        Converters = {new StringEnumConverter()}
    };

// Serialized as: {"day":"Saturday"}
await PostSomeStuff(new { Day = DayOfWeek.Saturday });

由于这些是全局设置,它们将影响您整个应用程序。可能会对隔离特定 API 的调用设置有所裨益。当创建 Refit 生成的实时接口时,您可以可选地传递一个 RefitSettings,这将允许您指定所需的序列化器设置。这使得您可以针对不同的 API 使用不同的序列化器设置。

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        ContentSerializer = new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings {
                ContractResolver = new SnakeCasePropertyNamesContractResolver()
        }
    )});

var otherApi = RestService.For<IOtherApi>("https://api.example.com",
    new RefitSettings {
        ContentSerializer = new NewtonsoftJsonContentSerializer(
            new JsonSerializerSettings {
                ContractResolver = new CamelCasePropertyNamesContractResolver()
        }
    )});

可以使用 Json.NET 的 JsonProperty 属性自定义属性序列化/反序列化。

public class Foo
{
    // Works like [AliasAs("b")] would in form posts (see below)
    [JsonProperty(PropertyName="b")]
    public string Bar { get; set; }
}
JSON 源生成器

要利用 .NET 6 中添加的新的 System.Text.Json 源生成器 的优势,您可以使用 SystemTextJsonContentSerializer 与一个自定义实例的 RefitSettingsJsonSerializerOptions

var options = new JsonSerializerOptions();
options.AddContext<MyJsonSerializerContext>();

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        ContentSerializer = new SystemTextJsonContentSerializer(options)
    });
XML 内容

XML 请求和响应是通过 System.Xml.Serialization.XmlSerializer 进行序列化和反序列化的。默认情况下,Refit 会使用 JSON 内容序列化,要使用 XML 内容配置内容序列化器以使用 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的构造函数提供一个XmlContentSerializerSettings来设置。

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
                    }
                }
            }
        )
    });
<a name="form-posts"></a>表单提交

对于需要表单提交(即序列化为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("别名")]别名属性名称,这在API具有难以理解的字段名时非常有帮助。

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);

如果您有一个具有[JsonProperty(PropertyName)]属性来设置属性别名的类型,Refit也会使用这些属性(如果有两者存在,则[AliasAs]将具有优先级)。这意味着以下类型将序列化为one=value1&two=value2


public class SomeObject
{
    [JsonProperty(PropertyName = "one")]
    public string FirstProperty { get; set; }

    [JsonProperty(PropertyName = "notTwo")]
    [AliasAs("two")]
    public string SecondProperty { get; set; }
}

注意:AliasAs的使用适用于查询字符串参数和表单体提交,但不适用于响应对象;对于响应对象中字段的别名,您仍然需要使用[JsonProperty("完整属性名称")]

设置请求头

静态头

您可以为请求设置一个或多个静态请求头,只需将一个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);
}
动态头

如果需要在运行时设置头内容,您可以通过将一个带有动态值的头添加到请求中,向参数应用Header属性来做到这一点。

[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");

添加Authorization头是一个非常常见的用例,因此您可以通过将Authorize属性应用到参数并可选地指定方案来将访问令牌添加到请求中。

[Get("/users/{user}")]
Task<User> GetUser(string user, [Authorize("Bearer")] string token);

// Will add the header "Authorization: Bearer OAUTH-TOKEN}" to the request
var user = await GetUser("octocat", "OAUTH-TOKEN");

//note: the scheme defaults to Bearer if none provided

如果您需要在运行时设置多个头,您可以通过将一个IDictionary<string, string>添加到参数并应用HeaderCollection属性来实现,这样它就会将头注入到请求中。


[Get("/users/{user}")]
Task<User> GetUser(string user, [HeaderCollection] IDictionary<string, string> headers);

var headers = new Dictionary<string, string> {{"Authorization","Bearer tokenGoesHere"}, {"X-Tenant-Id","123"}};
var user = await GetUser("octocat", headers);
Bearer 验证

大多数API都需要某种形式的身份验证。最常见的是OAuth Bearer身份验证。请求将添加一个头:Authorization: Bearer <token>。Refit使您能够轻松插入您自己的逻辑来获取令牌,您不需要将令牌传递到每个方法中。

  1. [Headers("Authorization: Bearer")]添加到需要令牌的接口或方法中。
  2. RefitSettings实例中设置AuthorizationHeaderValueGetter。当Refit需要获取令牌时,它会调用您的委托,因此让您的机制在令牌生命周期内缓存令牌值一段时间是个好主意。
使用 DelegatingHandlers 减少头部的样板代码(授权头示例)

尽管我们在Refit中直接提供了在运行时添加动态头的功能,但大多数用例可能从注册一个自定义DelegatingHandler中受益,以便将头作为HttpClient中间件管道的一部分注入,从而消除添加大量的[Header][HeaderCollection]属性的需求。

在上面的示例中,我们利用了一个[HeaderCollection]参数来注入一个AuthorizationX-Tenant-Id头。这在与使用OAuth2的第三方集成时是一个相当常见的场景。虽然对于偶尔的端点来说没问题,但如果我们在接口的每个方法中添加这样的样板代码,就太麻烦了。

在此示例中,我们将假设我们的应用程序是一个多租户应用程序,能够通过某种接口ITenantProvider获取有关租户的信息,并且有一个数据存储IAuthTokenStore,可以用来检索附加到出站请求的认证令牌。


 //Custom delegating handler for adding Auth headers to outbound requests
 class AuthHeaderHandler : DelegatingHandler
 {
     private readonly ITenantProvider tenantProvider;
     private readonly IAuthTokenStore authTokenStore;

    public AuthHeaderHandler(ITenantProvider tenantProvider, IAuthTokenStore authTokenStore)
    {
         this.tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider));
         this.authTokenStore = authTokenStore ?? throw new ArgumentNullException(nameof(authTokenStore));
         // InnerHandler must be left as null when using DI, but must be assigned a value when
         // using RestService.For<IMyApi>
         // InnerHandler = new HttpClientHandler();
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await authTokenStore.GetToken();

        //potentially refresh token here if it has expired etc.

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        request.Headers.Add("X-Tenant-Id", tenantProvider.GetTenantId());

        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

//Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<ITenantProvider, TenantProvider>();
    services.AddTransient<IAuthTokenStore, AuthTokenStore>();
    services.AddTransient<AuthHeaderHandler>();

    //this will add our refit api implementation with an HttpClient
    //that is configured to add auth headers to all requests

    //note: AddRefitClient<T> requires a reference to Refit.HttpClientFactory
    //note: the order of delegating handlers is important and they run in the order they are added!

    services.AddRefitClient<ISomeThirdPartyApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"))
        .AddHttpMessageHandler<AuthHeaderHandler>();
        //you could add Polly here to handle HTTP 429 / HTTP 503 etc
}

//Your application code
public class SomeImportantBusinessLogic
{
    private ISomeThirdPartyApi thirdPartyApi;

    public SomeImportantBusinessLogic(ISomeThirdPartyApi thirdPartyApi)
    {
        this.thirdPartyApi = thirdPartyApi;
    }

    public async Task DoStuffWithUser(string username)
    {
        var user = await thirdPartyApi.GetUser(username);
        //do your thing
    }
}

如果您没有使用依赖注入,您可以像这样做以实现相同的功能

var api = RestService.For<ISomeThirdPartyApi>(new HttpClient(new AuthHeaderHandler(tenantProvider, authTokenStore))
    {
        BaseAddress = new Uri("https://api.example.com")
    }
);

var user = await thirdPartyApi.GetUser(username);
//do your thing
重新定义头部

与Retrofit不同,Retrofit中头信息不会相互覆盖,无论相同的头信息定义多少次,它都会添加到请求中,而Refit采用了与ASP.NET MVC中类似操作过滤器的方法——重新定义头信息将覆盖它,以下为优先级顺序:

  • Headers属性在接口上(优先级最低)
  • Headers属性在方法上
  • 方法参数上的Header属性或HeaderCollection属性(优先级最高)
[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
    [Get("/users/list")]
    Task<List> GetUsers();

    [Get("/users/{user}")]
    [Headers("X-Emoji: :smile_cat:")]
    Task<User> GetUser(string user);

    [Post("/users/new")]
    [Headers("X-Emoji: :metal:")]
    Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji);
}

// X-Emoji: :rocket:
var users = await GetUsers();

// X-Emoji: :smile_cat:
var user = await GetUser("octocat");

// X-Emoji: :trollface:
await CreateUser(user, ":trollface:");

注意:这种重新定义行为仅适用于具有相同名称的头信息。不同名称的头信息不会替换。以下代码将导致包含所有头信息

[Headers("Header-A: 1")]
public interface ISomeApi
{
    [Headers("Header-B: 2")]
    [Post("/post")]
    Task PostTheThing([Header("Header-C")] int c);
}

// Header-A: 1
// Header-B: 2
// Header-C: 3
var user = await api.PostTheThing(3);
删除头部

在接口或方法上定义的头信息可以通过重新定义不带值(即不带: <value>)的静态头信息或传递动态头信息的null来删除。空字符串将被包含为空头信息。

[Headers("X-Emoji: :rocket:")]
public interface IGitHubApi
{
    [Get("/users/list")]
    [Headers("X-Emoji")] // Remove the X-Emoji header
    Task<List> GetUsers();

    [Get("/users/{user}")]
    [Headers("X-Emoji:")] // Redefine the X-Emoji header as empty
    Task<User> GetUser(string user);

    [Post("/users/new")]
    Task CreateUser([Body] User user, [Header("X-Emoji")] string emoji);
}

// No X-Emoji header
var users = await GetUsers();

// X-Emoji:
var user = await GetUser("octocat");

// No X-Emoji header
await CreateUser(user, null);

// X-Emoji:
await CreateUser(user, "");

将状态传递到 DelegatingHandlers

如果您需要在运行时将其传递给DelegatingHandler,可以将一个具有动态值的属性添加到底层HttpRequestMessage.Properties中,方法是为参数应用Property属性

public interface IGitHubApi
{
    [Post("/users/new")]
    Task CreateUser([Body] User user, [Property("SomeKey")] string someValue);

    [Post("/users/new")]
    Task CreateUser([Body] User user, [Property] string someOtherKey);
}

属性构造函数可选地接受一个字符串,该字符串将成为HttpRequestMessage.Properties字典的键。如果没有显式定义键,则参数的名称将成为键。如果定义了多次键,则HttpRequestMessage.Properties中的值将被覆盖。参数本身可以是任何object。属性可以在DelegatingHandler内以如下方式访问

class RequestPropertyHandler : DelegatingHandler
{
    public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // See if the request has a the property
        if(request.Properties.ContainsKey("SomeKey"))
        {
            var someProperty = request.Properties["SomeKey"];
            //do stuff
        }

        if(request.Properties.ContainsKey("someOtherKey"))
        {
            var someOtherProperty = request.Properties["someOtherKey"];
            //do stuff
        }

        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

注意:在.NET 5中,HttpRequestMessage.Properties已被标记为Obsolete,而Refit将相反将值填充到新的HttpRequestMessage.Options

对 Polly 和 Polly.Context 的支持

因为Refit支持HttpClientFactory,所以您可以为HttpClient配置Polly策略。如果您的策略使用Polly.Context,则可以通过向其添加[Property("PolicyExecutionContext")] Polly.Context context将此传递给Refit,因为在幕后Polly.Context只被存储在键为PolicyExecutionContextHttpRequestMessage.Properties之下,并且其类型为Polly.Context。如果您的用例需要将仅在下述内容已知时初始化的Polly.Context,则建议仅以这种方式传递Polly.Context。如果您的Polly.Context每次都需要相同的内容(例如一个ILogger,您想在其中使用策略进行日志记录),则一个更干净的方法是将Polly.Context通过DelegatingHandler注入,如#801中描述的那样。

目标接口类型和方法信息

有时您可能想知道Refit实例的目标接口类型。一个示例是有这样一个派生接口,它实现了类似这样的共同基础

public interface IGetAPI<TEntity>
{
    [Get("/{key}")]
    Task<TEntity> Get(long key);
}

public interface IUsersAPI : IGetAPI<User>
{
}

public interface IOrdersAPI : IGetAPI<Order>
{
}

您可以通过接口的实例类型来访问它,这可以在处理程序中使用,例如要更改请求的URL

class RequestPropertyHandler : DelegatingHandler
{
    public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Get the type of the target interface
        Type interfaceType = (Type)request.Properties[HttpMessageRequestOptions.InterfaceType];

        var builder = new UriBuilder(request.RequestUri);
        // Alter the Path in some way based on the interface or an attribute on it
        builder.Path = $"/{interfaceType.Name}{builder.Path}";
        // Set the new Uri on the outgoing message
        request.RequestUri = builder.Uri;

        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

完整的方法信息(RestMethodInfo)也始终在请求选项中可用。当使用反射时,RestMethodInfo包含有关正在调用的方法的更多信息,例如完整的MethodInfo

class RequestPropertyHandler : DelegatingHandler
{
    public RequestPropertyHandler(HttpMessageHandler innerHandler = null) : base(innerHandler ?? new HttpClientHandler()) {}

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        // Get the method info
        if (request.Options.TryGetValue(HttpRequestMessageOptions.RestMethodInfoKey, out RestMethodInfo restMethodInfo))
        {
            var builder = new UriBuilder(request.RequestUri);
            // Alter the Path in some way based on the method info or an attribute on it
            builder.Path = $"/{restMethodInfo.MethodInfo.Name}{builder.Path}";
            // Set the new Uri on the outgoing message
            request.RequestUri = builder.Uri;
        }

        return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
    }
}

注意:在.NET 5中,HttpRequestMessage.Properties已被标记为Obsolete,而Refit将相反将值填充到新的HttpRequestMessage.Options。Refit提供了HttpRequestMessageOptions.InterfaceTypeKeyHttpRequestMessageOptions.RestMethodInfoKey来相应地从选项中访问接口类型和REST方法信息。

多部分上传

带有Multipart属性的装饰方法将以多部分内容类型提交。目前,多部分方法支持以下参数类型

  • 字符串(参数名称将用作名称,字符串值用作值)
  • 字节数组
  • Stream
  • FileInfo

多部分数据中字段的名称及其优先级顺序

  • 如果指定且非空,则为multipartItem.Name(可选);动态,允许在执行时命名表单数据部分。
  • 装饰方法签名中streamPart参数的[AliasAs]属性(可选);静态,在代码中定义。
  • 方法签名中定义的MultipartItem参数名(默认值)为静态,在代码中定义。

可以使用一个可选的字符串参数指定一个自定义边界,该参数传递给Multipart属性。如果为空,则默认为----MyGreatBoundary

为了指定字节码数组(byte[])、StreamFileInfo参数的文件名和内容类型,需要使用包装类。这些类型的包装类是ByteArrayPartStreamPartFileInfoPart

public interface ISomeApi
{
    [Multipart]
    [Post("/users/{id}/photo")]
    Task UploadPhoto(int id, [AliasAs("myPhoto")] StreamPart stream);
}

要将流传递给此方法,请构建一个类似下面的StreamPart对象

someApiInstance.UploadPhoto(id, new StreamPart(myPhotoStream, "photo.jpg", "image/jpeg"));

注意:此节之前描述的 AttachmentName 属性已被弃用,其使用不建议。

检索响应

请注意,在 Refit 中,与 Retrofit 不同,没有同步网络请求的选项 - 所有请求都必须是异步的,要么通过Task,要么通过IObservable。与 Retrofit 不同,没有通过回调参数创建异步方法的选择,因为我们生活在异步/等待的未来。

与通过参数类型更改正文内容的方式类似,返回类型将决定返回的内容。

如果没有类型参数返回 Task,将丢弃内容,仅仅告诉你调用是否成功

[Post("/users/new")]
Task CreateUser([Body] User user);

// This will throw if the network call fails
await CreateUser(someUser);

如果类型参数是 'HttpResponseMessage' 或 'string',则分别返回原始响应消息或作为字符串的内容。

// Returns the content as a string (i.e. the JSON data)
[Get("/users/{user}")]
Task<string> GetUser(string user);

// Returns the raw response, as an IObservable that can be used with the
// Reactive Extensions
[Get("/users/{user}")]
IObservable<HttpResponseMessage> GetUser(string user);

还有一个名为ApiResponse<T>的泛型包装类,可以用作返回类型。将此类用作返回类型不仅可以检索内容作为对象,还可以检索与请求/响应相关的任何元数据。这包括响应头、http 状态码和原因短语(例如 404 Not Found)、响应版本、发送的原始请求消息,以及在出现错误的情况下,包含错误详细信息的ApiException对象。以下是如何检索响应元数据的示例。

//Returns the content within a wrapper class containing metadata about the request/response
[Get("/users/{user}")]
Task<ApiResponse<User>> GetUser(string user);

//Calling the API
var response = await gitHubApi.GetUser("octocat");

//Getting the status code (returns a value from the System.Net.HttpStatusCode enumeration)
var httpStatus = response.StatusCode;

//Determining if a success status code was received
if(response.IsSuccessStatusCode)
{
    //YAY! Do the thing...
}

//Retrieving a well-known header value (e.g. "Server" header)
var serverHeaderValue = response.Headers.Server != null ? response.Headers.Server.ToString() : string.Empty;

//Retrieving a custom header value
var customHeaderValue = string.Join(',', response.Headers.GetValues("A-Custom-Header"));

//Looping through all the headers
foreach(var header in response.Headers)
{
    var headerName = header.Key;
    var headerValue = string.Join(',', header.Value);
}

//Finally, retrieving the content in the response body as a strongly-typed object
var user = response.Content;

使用泛型接口

当使用类似 ASP.NET Web API 时,有一个非常常见的模式,那就是有一堆 CRUD REST 服务。现在 Refit 支持这些服务,允许您使用泛型类型定义单个 API 接口

public interface IReallyExcitingCrudApi<T, in TKey> where T : class
{
    [Post("")]
    Task<T> Create([Body] T payload);

    [Get("")]
    Task<List<T>> ReadAll();

    [Get("/{key}")]
    Task<T> ReadOne(TKey key);

    [Put("/{key}")]
    Task Update(TKey key, [Body]T payload);

    [Delete("/{key}")]
    Task Delete(TKey key);
}

它可以这样使用

// The "/users" part here is kind of important if you want it to work for more
// than one type (unless you have a different domain for each type)
var api = RestService.For<IReallyExcitingCrudApi<User, string>>("http://api.example.com/users");

接口继承

当多个需要保持独立的多个服务共享一些 API 时,可以通过利用接口继承来避免在不同服务中多次定义相同的 Refit 方法

public interface IBaseService
{
    [Get("/resources")]
    Task<Resource> GetResource(string id);
}

public interface IDerivedServiceA : IBaseService
{
    [Delete("/resources")]
    Task DeleteResource(string id);
}

public interface IDerivedServiceB : IBaseService
{
    [Post("/resources")]
    Task<string> AddResource([Body] Resource resource);
}

在此示例中,IDerivedServiceA接口将公开GetResourceDeleteResource API,而IDerivedServiceB将公开GetResourceAddResource

头部继承

在继承中,现有的头属性也会传递,且最内层的属性具有优先级

[Headers("User-Agent: AAA")]
public interface IAmInterfaceA
{
    [Get("/get?result=Ping")]
    Task<string> Ping();
}

[Headers("User-Agent: BBB")]
public interface IAmInterfaceB : IAmInterfaceA
{
    [Get("/get?result=Pang")]
    [Headers("User-Agent: PANG")]
    Task<string> Pang();

    [Get("/get?result=Foo")]
    Task<string> Foo();
}

在这里,IAmInterfaceB.Pang()将使用PANG作为其用户代理,而IAmInterfaceB.FooIAmInterfaceB.Ping将使用BBB。请注意,如果没有声明IAmInterfaceB的头部属性,那么Foo将使用从IAmInterfaceA继承而来的AAA值。如果一个接口继承了多个接口,那么优先顺序与继承接口声明的顺序相同

public interface IAmInterfaceC : IAmInterfaceA, IAmInterfaceB
{
    [Get("/get?result=Foo")]
    Task<string> Foo();
}

在这里,如果存在从 IAmInterfaceA 继承而来的头部属性,则 IAmInterfaceC.Foo 将使用该属性,否则使用从 IAmInterfaceB 继承的属性,依次类推,直到所有的声明接口。

默认接口方法

C# 8.0 开始,可以在接口上定义默认接口方法(也称为 DIMs)。Refit 接口可以提供使用 DIMs 的附加逻辑,可选地结合使用私有和/或静态辅助方法

public interface IApiClient
{
    // implemented by Refit but not exposed publicly
    [Get("/get")]
    internal Task<string> GetInternal();
    // Publicly available with added logic applied to the result from the API call
    public async Task<string> Get()
        => FormatResponse(await GetInternal());
    private static String FormatResponse(string response)
        => $"The response is: {response}";
}

Refit生成的类型将实现方法IApiClient.GetInternal。如果在调用前后需要额外的逻辑,则不应直接暴露,可以通过将其标记为internal来隐藏,从而避免对消费者的暴露。默认接口方法IApiClient.Get将被所有实现IApiClient的类型继承,包括当然也包括由Refit生成的类型。IApiClient的消费者将调用公共的Get方法,并从其实现中受益(在此情况下,可使用私有静态辅助函数FormatResponse)。为了支持不提供DIM支持的平台(.NET Core 2.x及以下或.NET Standard 2.0及以下),此解决方案需要两个额外的类型。

internal interface IApiClientInternal
{
    [Get("/get")]
    Task<string> Get();
}
public interface IApiClient
{
    public Task<string> Get();
}
internal class ApiClient : IApiClient
{
    private readonly IApiClientInternal client;
    public ApiClient(IApiClientInternal client) => this.client = client;
    public async Task<string> Get()
        => FormatResponse(await client.Get());
    private static String FormatResponse(string response)
        => $"The response is: {response}";
}

使用 HttpClientFactory

Refit对ASP.Net Core 2.1 HttpClientFactory提供了一等支持。在您的ConfigureServices方法中添加对Refit.HttpClientFactory的引用,并调用提供扩展方法来配置您的Refit接口。

services.AddRefitClient<IWebApi>()
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
        // Add additional IHttpClientBuilder chained methods as required here:
        // .AddHttpMessageHandler<MyHandler>()
        // .SetHandlerLifetime(TimeSpan.FromMinutes(2));

可选地,可以包含一个RefitSettings对象。

var settings = new RefitSettings();
// Configure refit settings here

services.AddRefitClient<IWebApi>(settings)
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
        // Add additional IHttpClientBuilder chained methods as required here:
        // .AddHttpMessageHandler<MyHandler>()
        // .SetHandlerLifetime(TimeSpan.FromMinutes(2));

// or injected from the container
services.AddRefitClient<IWebApi>(provider => new RefitSettings() { /* configure settings */ })
        .ConfigureHttpClient(c => c.BaseAddress = new Uri("https://api.example.com"));
        // Add additional IHttpClientBuilder chained methods as required here:
        // .AddHttpMessageHandler<MyHandler>()
        // .SetHandlerLifetime(TimeSpan.FromMinutes(2));

请注意,由于HttpClientHttpClientHandlers将由HttpClientFactory管理而非Refit,因此RefitSettings的一些属性将被忽略。

然后,您可以使用构造函数注入来获取API界面。

public class HomeController : Controller
{
    public HomeController(IWebApi webApi)
    {
        _webApi = webApi;
    }

    private readonly IWebApi _webApi;

    public async Task<IActionResult> Index(CancellationToken cancellationToken)
    {
        var thing = await _webApi.GetSomethingWeNeed(cancellationToken);
        return View(thing);
    }
}

提供自定义 HttpClient

您可以通过将自定义HttpClient实例作为参数传递给RestService.For<T>方法来提供自定义的HttpClient实例。

RestService.For<ISomeApi>(new HttpClient()
{
    BaseAddress = new Uri("https://www.someapi.com/api/")
});

但是,在提供自定义的HttpClient实例时,以下RefitSettings属性将无法工作:

  • AuthorizationHeaderValueGetter
  • HttpMessageHandlerFactory

如果还想在上述设置的同时配置Refit提供的HttpClient实例,只需在API接口上暴露HttpClient即可。

interface ISomeApi
{
    // This will automagically be populated by Refit if the property exists
    HttpClient Client { get; }

    [Headers("Authorization: Bearer")]
    [Get("/endpoint")]
    Task<string> SomeApiEndpoint();
}

然后,在创建了REST服务后,您可以设置任何HttpClient属性,例如Timeout

SomeApi = RestService.For<ISomeApi>("https://www.someapi.com/api/", new RefitSettings()
{
    AuthorizationHeaderValueGetter = (rq, ct) => GetTokenAsync()
});

SomeApi.Client.Timeout = timeout;

处理异常

Refit的异常处理行为取决于您的Refit接口方法返回的是否为Task<T>Task<IApiResponse>Task<IApiResponse<T>>,或Task<ApiResponse<T>>

<a id="when-returning-taskapiresponset"></a>当返回类型为Task<IApiResponse>Task<IApiResponse<T>>Task<ApiResponse<T>>

Refit会在处理响应时截获由ExceptionFactory引发的任何ApiException,以及尝试将响应反序列化到ApiResponse<T>时发生的任何错误,并将在ApiResponse<T>Error属性中填充异常,而不会抛出异常。

然后您可以根据需要进行决定

var response = await _myRefitClient.GetSomeStuff();
if(response.IsSuccessStatusCode)
{
   //do your thing
}
else
{
   _logger.LogError(response.Error, response.Error.Content);
}
当返回类型为Task<T>

Refit会抛出处理响应时由ExceptionFactory引发的任何ApiException,以及尝试将响应反序列化到Task<T>时发生的任何错误。

// ...
try
{
   var result = await awesomeApi.GetFooAsync("bar");
}
catch (ApiException exception)
{
   //exception handling
}
// ...

Refit还可以抛出ValidationApiException,这除了包含在ApiException上的信息之外,还包含ProblemDetails。当服务实现了RFC 7807规范的问题详细信息,并且响应内容类型为application/problem+json

有关验证异常中问题详细信息的特定信息,只需捕获ValidationApiException即可

// ...
try
{
   var result = await awesomeApi.GetFooAsync("bar");
}
catch (ValidationApiException validationException)
{
   // handle validation here by using validationException.Content,
   // which is type of ProblemDetails according to RFC 7807

   // If the response contains additional properties on the problem details,
   // they will be added to the validationException.Content.Extensions collection.
}
catch (ApiException exception)
{
   // other exception handling
}
// ...
提供自定义的ExceptionFactory

您还可以通过在RefitSettings中提供自定义异常工厂来覆盖默认的异常行为,默认异常是由ExceptionFactory在处理结果时引发的。例如,您可以使用以下方式抑制所有异常

var nullTask = Task.FromResult<Exception>(null);

var gitHubApi = RestService.For<IGitHubApi>("https://api.github.com",
    new RefitSettings {
        ExceptionFactory = httpResponse => nullTask;
    });

请注意,尝试反序列化响应时引发的异常不受此影响。

ApiException解构与Serilog

对于使用 Serilog 的用户,您可以使用 Serilog.Exceptions.Refit NuGet 包来丰富 ApiException 的日志记录。有关如何将此包集成到您的应用程序中的详细信息,请参阅此处

产品 兼容的以及额外的计算目标框架版本。
.NET net5.0 已计算。 net5.0-windows 已计算。 net6.0 兼容。 net6.0-android 已计算。 net6.0-ios 已计算。 net6.0-maccatalyst 已计算。 net6.0-macos 已计算。 net6.0-tvos 已计算。 net6.0-windows 已计算。 net7.0 已计算。 net7.0-android 已计算。 net7.0-ios 已计算。 net7.0-maccatalyst 已计算。 net7.0-macos 已计算。 net7.0-tvos 已计算。 net7.0-windows 已计算。 net8.0 兼容。 net8.0-android 已计算。 net8.0-browser 已计算。 net8.0-ios 已计算。 net8.0-maccatalyst 已计算。 net8.0-macos 已计算。 net8.0-tvos 已计算。 net8.0-windows 已计算。
.NET Core netcoreapp2.0 已计算。 netcoreapp2.1 已计算。 netcoreapp2.2 已计算。 netcoreapp3.0 已计算。 netcoreapp3.1 已计算。
.NET Standard netstandard2.0 兼容。 netstandard2.1 已计算。
.NET Framework net461 已计算。 net462 兼容。 net463 已计算。 net47 已计算。 net471 已计算。 net472 已计算。 net48 已计算。 net481 已计算。
MonoAndroid monoandroid 已计算。
MonoMac monomac 已计算。
MonoTouch monotouch 已计算。
Tizen tizen40 已计算。 tizen60 已计算。
Xamarin.iOS xamarinios 已计算。
Xamarin.Mac xamarinmac 已计算。
Xamarin.TVOS xamarintvos 已计算。
Xamarin.WatchOS xamarinwatchos 已计算。
兼容的目标框架
包含的目标框架(在包中)
了解更多关于目标框架.NET Standard的信息。

NuGet 包 (67)

显示依赖 Refit.Newtonsoft.Json 的前 5 个 NuGet 包

下载
N3O.Umbraco.Extensions

TODO

Apple.Receipt.Verificator

用于验证 App Store 收据的库

Meraki.Api

Meraki API

Elsa.Client

Elsa 是一组工作流库和工具,可在任何 .NET Core 应用程序中实现轻量级和高效的工作流功能。此包为 Elsa 的 REST API 端点提供了使用 Refit 的客户端。

EmsApi.Client

Event Measurement System API 的 .NET 客户端库

GitHub 仓库 (7)

显示依赖 Refit.Newtonsoft.Json 的前 5 个流行的 GitHub 仓库

仓库 Stars
ErsatzTV/ErsatzTV
使用您自己的媒体流自定义实时频道
brminnick/GitTrends
A iOS 和 Android 应用,用于监视 GitHub 仓库的查看、克隆和 Star 历史
dorisoy/Dorisoy.Pan
Dorisoy.Pan 是一个基于 .NET Core 8 的跨平台文档管理系统,使用 MS SQL 2012/MySql8.0(或更高版本)作为后端数据库,您可以在 Windows、Linux 或 Mac 上运行它。项目中的所有方法都是异步的,支持基于令牌的身份验证,项目架构遵循著名的软件模式和最佳安全实践。源代码是完全可定制的,模块化且结构清晰,使开发定制功能和满足任何业务需求变得容易。系统使用最新的 Microsoft 技术,提供高性能、稳定性和安全性。
whuanle/maomi
Maomi 框架是一个简单、简洁的开发框架,除了框架本身提供的功能外,Maomi 还作为一个易于阅读的开源项目,为开发者提供设计框架的思路和代码。
Strypper/mauisland
MAUIsland 🏝️ 是 .NET MAUI 的 No.1 控件库
版本 下载 最后更新
7.1.2 120,993 6/30/2024
7.1.1 29,651 6/24/2024
7.1.0 15,752 6/20/2024
7.0.0 2,662,836 6/29/2023
7.0.0-beta.1 4,442 5/14/2023
6.3.2 9,490,984 2/8/2022
6.2.16 232,780 1/26/2022
6.1.15 1,330,098 10/14/2021
6.0.94 642,887 8/5/2021
6.0.38 799,339 3/19/2021
6.0.24 126,471 2/23/2021
6.0.21 499 2/23/2021
6.0.15 38,904 2/11/2021
6.0.8 15,633 2/10/2021
6.0.1 39,642 2/8/2021
6.0.0-preview.128 227 2/4/2021
6.0.0-preview.124 204 2/4/2021
6.0.0-preview.121 212 2/4/2021
6.0.0-preview.96 2,081 1/25/2021
6.0.0-preview.94 215 1/24/2021
6.0.0-preview.86 256 1/24/2021
6.0.0-preview.84 233 1/23/2021
6.0.0-preview.37 1,464 11/26/2020
6.0.0-preview.34 1,684 11/26/2020