.Net Core 扩展使用Refit

标签(空格分隔): 未分类


在.net core 2.1当中,目前可以是用HttpClientFactory进行Http的调用,它的使用方法我不再多说,具体参见(https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/http-requests?view=aspnetcore-2.2#consumption-patterns)。微软的官网写的非常详细了。

在调用过程中使用Refit可以进行强类型的客户端调用,个人比较喜欢这个。目前还有一个组件是老九大佬写的一个类似的客户端(https://github.com/dotnetcore/WebApiClient)

在WebApi当中使用Refit。

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

            services.AddHttpClient("github", c =>
            {
                c.BaseAddress = new Uri("https://api.github.com");
            })
    .AddTypedClient(c => Refit.RestService.For<IGitHubApi>(c));

            services.AddRefitClient<IGitHubApi>();
        }

        

接口的定义为:

    [Headers("User-Agent: Refit Integration Tests")]
    //[RefitClient("github")]
    public interface IGitHubApi
    {
        [Get("/users/{username}")]
        Task<User> GetUser(string userName);
    }

当我们有很多接口需要注入的时候,就得写很多的类似强类型的AddRefitClient的代码,个人觉得非常的麻烦。是否能够根据标签程序集进行批量注入。
Refit的注入,其实注入的是一个RestService

        public static IHttpClientBuilder AddRefitClient<T>(this IServiceCollection services, RefitSettings settings = null) where T : class
        {
            services.AddSingleton(provider => RequestBuilder.ForType<T>(settings));

            return services.AddHttpClient(UniqueName.ForType<T>())
                           .AddTypedClient((client, serviceProvider) => RestService.For<T>(client, serviceProvider.GetService<IRequestBuilder<T>>()));
        }

具体的类型是在代码编译阶段用BuildTask生成了带有RefitHttpAttribute的接口类型的实体类。

具体创建的类型如下:

[ExcludeFromCodeCoverage, DebuggerNonUserCode, Preserve]
internal class AutoGeneratedIGitHubApi : IGitHubApi
{
    // Fields
    [CompilerGenerated, DebuggerBrowsable(0)]
    private HttpClient <Client>k__BackingField;
    private readonly IRequestBuilder requestBuilder;

    // Methods
    public AutoGeneratedIGitHubApi(HttpClient client, IRequestBuilder requestBuilder)
    {
        this.Client = client;
        this.requestBuilder = requestBuilder;
    }

    public virtual Task<User> GetUser(string userName)
    {
        object[] objArray1 = new object[] { userName };
        object[] objArray = objArray1;
        Type[] typeArray1 = new Type[] { typeof(string) };
        return (Task<User>) this.requestBuilder.BuildRestResultFuncForMethod("GetUser", typeArray1, null)(this.Client, objArray);
    }

    // Properties
    public HttpClient Client { get; protected set; }
}

目前我们要解决的是如何批量的进行注入的问题:个人在Refit的源码上添加了一些代码以支持批量注入

   //配置的实体
    public class ApiSettings
    {
        public List<ApiOption> Apis { get; set; } = new List<ApiOption>();

    }

    public class ApiOption
    {

        public string Name { get; set; }

        public string Version { get; set; }

        //httpClient的baseAddress的配置
        public string Url { get; set; }
    }
    //配置的搜索的程序集的名称
    public class DISettings
    {
        public List<string> RefitClientAssemblyNames { get; set; } = new List<string>();
    }
    /// <summary>
    /// 批量注入时打上这个标签标明是Refit的接口
    /// </summary>
    public class RefitClientAttribute : Attribute
    {
        /// <summary>
        /// appsetting中的apiname
        /// </summary>
        public string ApiName { get; set; }
        public RefitClientAttribute(string apiName)
        {
            ApiName = apiName;
        }
    }
    /// <summary>
    /// 基于HttpClient的扩展方法
    /// </summary>
    public static class RefitHttpClientExtension
    {
        public static IServiceCollection ScanAddHttpClient(this IServiceCollection serviceCollection, ApiSettings settings)
        {
            #region 批量注入HttpClient
            if (settings == null)
            {
                throw new ArgumentNullException("settings");
            }
            var apis = settings.Apis;
            if (apis == null || apis.Count == 0)
            {
                throw new ArgumentNullException("settings.Apis");
            }
            foreach (var ass in apis)
            {
                serviceCollection.AddHttpClient(ass.Name).ConfigureHttpClient(client => client.BaseAddress = new Uri(ass.Url));
            }
            #endregion
            return serviceCollection;
        }
        public static IServiceCollection ScanAddRefitClient(this IServiceCollection serviceCollection, IConfiguration configration, RefitSettings refitSettings = null)
        {
            DISettings dISettings = configration.GetSection("DISettings").Get<DISettings>();
            if (dISettings == null)
            {
                throw new ArgumentNullException("dISettings", "配置文件中不存在DISettings节点");
            }
            if (dISettings == null || dISettings.RefitClientAssemblyNames == null || dISettings.RefitClientAssemblyNames.Count == 0)
            {
                throw new ArgumentNullException("dISettings.RefitClientAssemblyNames", "配置文件DISettings节点不存在RefitClientAssemblyNames节点!!");
            }
            ApiSettings apiSettings = configration.GetSection("ApiSettings").Get<ApiSettings>();
            if (apiSettings == null)
            {
                throw new ArgumentNullException("apiSettings", "配置文件中不存在ApiSettings节点");
            }
            if (apiSettings.Apis == null || apiSettings.Apis.Count == 0)
            {
                throw new ArgumentNullException("apiSettings.Apis", "配置文件ApiSettings节点不存在Apis节点!!");
            }
            return serviceCollection.ScanAddRefitClient(apiSettings, dISettings, refitSettings);
        }


        public static IServiceCollection ScanAddRefitClient(this IServiceCollection serviceCollection, ApiSettings apiSettings, DISettings dISettings, RefitSettings refitSettings = null)
        {
            #region 批量注入HttpClient
            serviceCollection.ScanAddHttpClient(apiSettings);
            #endregion

            var registTypes = GetAttributeType(dISettings);
            serviceCollection.ScanAddRefitClient(registTypes, refitSettings);
            return serviceCollection;
        }

        public static IServiceCollection ScanAddRefitClient(this IServiceCollection serviceCollection, List<Type> registTypes, RefitSettings refitSettings = null)
        {
            foreach (var type in registTypes)
            {
                var name = type.GetCustomAttribute<RefitClientAttribute>().ApiName;
                serviceCollection.AddRefitClient(type, name, refitSettings);
            }
            return serviceCollection;
        }

        private static List<Type> GetAttributeType(DISettings options)
        {
            var registTypes = new List<Type>();
            var assemblys = options.RefitClientAssemblyNames;
            foreach (var assembly in assemblys)
            {
                registTypes.AddRange(GetAssemblyType(assembly, type =>
                {
                    var refitAttrs = type.GetCustomAttributes<RefitClientAttribute>();
                    if (refitAttrs == null || refitAttrs.Count() == 0)
                    {
                        return false;
                    }
                    else
                    {
                        return true;
                    }
                }));
            }
            return registTypes;
        }

        private static List<Type> GetAssemblyType(string assemblyName, Predicate<Type> predicate = null)
        {
            var registTypes = new List<Type>();
            var ass = Assembly.Load(new AssemblyName(assemblyName));
            var types = ass.DefinedTypes.Where(p => p.IsPublic && p.IsInterface).Select(x => x.AsType())?.ToList();
            foreach (var type in types)
            {
                if (predicate == null)
                {
                    registTypes.Add(type);
                    continue;
                }
                if (predicate(type))
                {
                    registTypes.Add(type);
                }
            }
            return registTypes;
        }

    }
    

在HttpClientFactoryExtensions扩展类中新增了两个基于Type的扩展方法而不是泛型的扩展方法

 /// <summary>
        /// Adds a Refit client to the DI container
        /// </summary>
        /// <param name="services">container</param>
        /// <param name="t">type of refit interface</param>
        /// <param name="httpclientName">httpclient's name</param>
        /// <param name="settings">Optional. Settings to configure the instance with</param>
        /// <returns></returns>
        public static IServiceCollection AddRefitClient(this IServiceCollection services,Type t,string httpclientName, RefitSettings settings = null)
        {
            var builder = RequestBuilder.ForType(t, settings);
            services.AddSingleton(provider => builder);
            services.AddSingleton(t, provider =>
            {
                var client=provider.GetService<IHttpClientFactory>().CreateClient(httpclientName);
                if (client == null)
                {
                    throw new Exception($"please inject the httpclient  named {httpclientName} httpclient!! ");
                }
                return RestService.For(client, builder, t);
            });
            return services;
        }


        /// <summary>
        /// Adds a Refit client to the DI container
        /// </summary>
        /// <param name="services">container</param>
        /// <param name="t">type of refit interface</param>
        /// <param name="client>httpclient</param>
        /// <param name="settings">Optional. Settings to configure the instance with</param>
        /// <returns></returns>
        public static IServiceCollection AddRefitClient(this IServiceCollection services, Type t, HttpClient client,RefitSettings settings = null)
        {
            var builder = RequestBuilder.ForType(t, settings);
            services.AddSingleton(provider => builder);
            services.AddSingleton(t, provider =>
            {
                return RestService.For(client, builder, t);
            });
            return services;
        }

第三:在是基于Refit的源码RestService改写下能够支持RestService.For(Type interType)的方法

        public static object For(HttpClient client, IRequestBuilder builder,Type t)
        {
            var generatedType = typeMapping.GetOrAdd(t, GetGeneratedType(t));

            return Activator.CreateInstance(generatedType, client, builder);
        }
        //它就是在这里获取生成好的接口的注入类型的名字
        static Type GetGeneratedType(Type t)
        {
            string typeName = UniqueName.ForType(t);

            var generatedType = Type.GetType(typeName);

            if (generatedType == null)
            {
                var message = t.Name + " doesn't look like a Refit interface. Make sure it has at least one " + "method with a Refit HTTP method attribute and Refit is installed in the project.";

                throw new InvalidOperationException(message);
            }

            return generatedType;
        }

        //获取名字方法也是泛型的也要加一个基于Type的
        public static string ForType(Type t)
        {
            string typeName;

            if (t.IsNested)
            {
                var className = "AutoGenerated" + t.DeclaringType.Name + t.Name;
                typeName = t.AssemblyQualifiedName.Replace(t.DeclaringType.FullName + "+" + t.Name, t.Namespace + "." + className);
            }
            else
            {
                var className = "AutoGenerated" + t.Name;

                if (t.Namespace == null)
                {
                    className = $"{className}.{className}";
                }

                typeName = t.AssemblyQualifiedName.Replace(t.Name, className);
            }

            return typeName;
        }

在AppSetting文件当中配置一些配置:

{
  "ApiSettings": {
    "Apis": [
      {
        "Name": "gitHubApi",
        "Version": "",
        "Url": "https://api.github.com"
      }
    ]
  },
  "DISettings": {
    "RefitClientAssemblyNames": [ "RefitWebTest" ]
  }
}

Refit的接口主要打上标签即可

    [Headers("User-Agent: Refit Integration Tests")]
    //参数的名字和配置文件的Apis:Name一致即可
    [RefitClient("gitHubApi")]
    public interface IGitHubApi
    {
        [Get("/users/{username}")]
        Task<User> GetUser(string userName);
    }

改写以后的使用:

            //批量注入带有RefitClientAttribute标签的接口
            services.ScanAddRefitClient(Configuration);
            

这样就实现了基于Refit的批量注入,注入的HttpClient的名字为配置文件当中的名字。

最后打包成Nuget包才能正确的使用,个人尝试发现必须Nuget包的名字是Refit才能正确的生成接口的代理类,(我知道改包名不是很好=。=)改个nuget的名字都不行,不知道为什么不知道有没有大佬知道,望指教!!
贴一下Refit的BuildTask的targets文件

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <CoreCompileDependsOn>
      $(CoreCompileDependsOn);
      GenerateRefitStubs;
    </CoreCompileDependsOn>
  </PropertyGroup>

  <PropertyGroup>
    <IntermediateOutputPath Condition="$(IntermediateOutputPath) == '' Or $(IntermediateOutputPath) == '*Undefined*'">$(MSBuildProjectDirectory)obj\$(Configuration)\</IntermediateOutputPath>
    <RefitTaskAssemblyFile Condition="'$(MSBuildRuntimeType)' == 'Core'">$(MSBuildThisFileDirectory)..\MSBuildCore20\InterfaceStubGenerator.BuildTasks.dll</RefitTaskAssemblyFile>
    <RefitTaskAssemblyFile Condition="'$(MSBuildRuntimeType)' != 'Core'">$(MSBuildThisFileDirectory)..\MSBuildFull46\InterfaceStubGenerator.BuildTasks.dll</RefitTaskAssemblyFile>

    <RefitMinCoreVersionRequired>2.0</RefitMinCoreVersionRequired>
    <!-- Our default CLI version for error checking purposes -->
    <RefitNetCoreAppVersion>$(BundledNETCoreAppTargetFrameworkVersion)</RefitNetCoreAppVersion>
    <RefitNetCoreAppVersion Condition="'$(RefitNetCoreAppVersion)' == ''">1.0</RefitNetCoreAppVersion>

    <!--
    Refit internal namespace to be added to internally generated Refit code.
    Can be overriden by user in case of namespace clashes.
    -->
    <RefitInternalNamespace Condition=" '$(RefitInternalNamespace)' == '' ">$(RootNamespace)</RefitInternalNamespace>
  </PropertyGroup>

  <UsingTask TaskName="Refit.Generator.Tasks.GenerateStubsTask" AssemblyFile="$(RefitTaskAssemblyFile)" />

  <Target Name="GenerateRefitStubs" BeforeTargets="CoreCompile">
    <Error Condition="'$(MSBuildRuntimeType)' == 'Core' and '$(RefitMinCoreVersionRequired)' > '$(RefitNetCoreAppVersion)' "
           Text="Refit requires at least the .NET Core SDK v2.0 to run with 'dotnet build'"
           ContinueOnError="false"
           />

    <GenerateStubsTask SourceFiles="@(Compile)"
                       BaseDirectory="$(MSBuildProjectDirectory)"
                       OutputFile="$(IntermediateOutputPath)\RefitStubs.g.cs"
                       RefitInternalNamespace="$(RefitInternalNamespace)"
                       />

    <Message Text="Processed Refit Stubs" />

    <ItemGroup Condition="Exists('$(IntermediateOutputPath)\RefitStubs.g.cs')">
        <Compile Include="$(IntermediateOutputPath)\RefitStubs.g.cs" />
    </ItemGroup>
  </Target>

</Project>

测试发现,只要改了打包的报名称,这个Task就不执行了,不知道为啥!!
如有知道忘告知!!

01-15 01:17