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
<PackageReference Include="Refit.Newtonsoft.Json" Version="7.1.2" />
paket add Refit.Newtonsoft.Json --version 7.1.2
#r "nuget: Refit.Newtonsoft.Json, 7.1.2"
// 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:适用于.NET Core、Xamarin和.NET的自动类型安全的REST库
Refit | Refit.HttpClientFactory | Refit.Newtonsoft.Json | |
---|---|---|---|
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"));
目录
- 这个功能在哪里工作?
- API 属性
- 查询字符串
- 正文内容
- 设置请求头
- 将状态传递到 DelegatingHandlers
- 多部分上传
- 检索响应
- 使用泛型接口
- 接口继承
- 默认接口方法
- 使用 HttpClientFactory
- 提供自定义 HttpClient
- 处理异常
这个功能在哪里工作?
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
设置为 NewtonsoftJsonContentSerializer
。System.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"
自定义查询字符串参数格式化
格式化键
要自定义查询键的格式,您有两个主要选项
使用
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
使用
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
与一个自定义实例的 RefitSettings
和 JsonSerializerOptions
。
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使您能够轻松插入您自己的逻辑来获取令牌,您不需要将令牌传递到每个方法中。
- 将
[Headers("Authorization: Bearer")]
添加到需要令牌的接口或方法中。 - 在
RefitSettings
实例中设置AuthorizationHeaderValueGetter
。当Refit需要获取令牌时,它会调用您的委托,因此让您的机制在令牌生命周期内缓存令牌值一段时间是个好主意。
使用 DelegatingHandlers 减少头部的样板代码(授权头示例)
尽管我们在Refit中直接提供了在运行时添加动态头的功能,但大多数用例可能从注册一个自定义DelegatingHandler
中受益,以便将头作为HttpClient
中间件管道的一部分注入,从而消除添加大量的[Header]
或[HeaderCollection]
属性的需求。
在上面的示例中,我们利用了一个[HeaderCollection]
参数来注入一个Authorization
和X-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
只被存储在键为PolicyExecutionContext
的HttpRequestMessage.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.InterfaceTypeKey
和HttpRequestMessageOptions.RestMethodInfoKey
来相应地从选项中访问接口类型和REST方法信息。
多部分上传
带有Multipart
属性的装饰方法将以多部分内容类型提交。目前,多部分方法支持以下参数类型
- 字符串(参数名称将用作名称,字符串值用作值)
- 字节数组
- Stream
- FileInfo
多部分数据中字段的名称及其优先级顺序
- 如果指定且非空,则为
multipartItem.Name
(可选);动态,允许在执行时命名表单数据部分。 - 装饰方法签名中
streamPart
参数的[AliasAs]
属性(可选);静态,在代码中定义。 - 方法签名中定义的
MultipartItem
参数名(默认值)为静态,在代码中定义。
可以使用一个可选的字符串参数指定一个自定义边界,该参数传递给Multipart
属性。如果为空,则默认为----MyGreatBoundary
。
为了指定字节码数组(byte[]
)、Stream
和FileInfo
参数的文件名和内容类型,需要使用包装类。这些类型的包装类是ByteArrayPart
、StreamPart
和FileInfoPart
。
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
接口将公开GetResource
和DeleteResource
API,而IDerivedServiceB
将公开GetResource
和AddResource
。
头部继承
在继承中,现有的头属性也会传递,且最内层的属性具有优先级
[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.Foo
和IAmInterfaceB.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));
请注意,由于HttpClient
和HttpClientHandlers
将由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 已计算。 |
-
.NETFramework 4.6.2
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
-
.NETStandard 2.0
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
-
net6.0
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
-
net8.0
- Newtonsoft.Json (>= 13.0.3)
- Refit (>= 7.1.2)
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 |