zoukankan      html  css  js  c++  java
  • .NET Core webapi路由

    简介

    定义:路由负责匹配传入的 HTTP 请求,然后将这些请求发送到应用的可执行终结点。 终结点是应用的可执行请求处理代码单元。 终结点在应用中进行定义,并在应用启动时进行配置。 终结点匹配过程可以从请求的 URL 中提取值,并为请求处理提供这些值。 通过使用应用中的终结点信息,路由还能生成映射到终结点的 URL。

    在ASP.NET Core中是使用路由中间件来匹配传入请求的 URL 并将它们映射到操作(action方法)。路由是在程序启动时进行传统路由或属性路由定义。 路由描述如何将 URL 路径与操作相匹配。 它还用于在响应中生成送出的 URL(用于链接)。

    路由操作既支持传统路由,也支持属性路由。也可混合使用。通常传统路由用于为浏览器处理 HTML 页面的控制器。属性路由用于处理 web API 的控制器。

    官方文档:https://docs.microsoft.com/zh-cn/aspnet/core/fundamentals/routing?view=aspnetcore-3.1

    web路由:https://docs.microsoft.com/zh-cn/aspnet/core/mvc/controllers/routing?view=aspnetcore-3.1

    路由中间件
    //向中间件管道添加路由匹配。 此中间件会查看应用中定义的终结点集,并根据请求选择最佳匹配。
    app.UseRouting();
    
    //向中间件管道添加终结点执行。 它会运行与所选终结点关联的委托。(通过map..等方法定义终结点)
    app.UseEndpoints(endpoints =>
    {
    
        //映射默认路由   {controller=Home}/{action=Index}/{id?}
        //endpoints.MapDefaultControllerRoute();
        //endpoints.MapControllerRoute(
        //          name: "default",
        //          pattern: "{controller=Home}/{action=Index}/{id?}");
    
    
        //endpoints.MapControllerRoute("api", "api/{controller}/{action}");
    
        //使用RouteAttribute
        endpoints.MapControllers();
    
    
    
    
        //添加终结点 可以理解mapget是一个终结点  (通过匹配 URL 和 HTTP 方法来选择。  通过运行委托来执行。  进而讲请求连接到路由系统)
        endpoints.MapGet(
            "/hello/{name:alpha}",//路由模板 用于配置终结点的匹配方式  :  是路由约束。 URL 路径的第二段 {name:alpha}绑定到 name 参数,捕获并存储在 HttpRequest. RouteValues 中
            async context =>
        {
            var name = context.Request.RouteValues["name"];
            await context.Response.WriteAsync($"Hello {name}!");
        });
    
    
        //当 HTTP GET 请求发送到根 URL / 时:
        //将执行显示的请求委托。
        //Hello World!会写入 HTTP 响应。 默认情况下,根 URL / 为 https://localhost:5001/。
        //如果请求方法不是 GET 或根 URL 不是 /,则无路由匹配,并返回 HTTP 404。
        endpoints.MapGet("/", async context =>
        {
            await context.Response.WriteAsync("Hello World!");
        });
    
    });

    UseRouting 向中间件管道添加路由匹配。 此中间件会查看应用中定义的终结点集,并根据请求选择最佳匹配。
    UseEndpoints 向中间件管道添加终结点执行。 它会运行与所选终结点关联的委托。

    调用 UseAuthentication 和 UseAuthorization 会添加身份验证和授权中间件。 这些中间件位于 UseRouting 和 UseEndpoints 之间,因此它们可以:

    • 查看 UseRouting 选择的终结点。
    • 将 UseEndpoints 发送到终结点之前应用授权策略。
    终结点

    我们说路由的根本目的是将用户请求地址,映射为一个请求处理器,最简单的请求处理器可以是一个委托 Func<HttpCotnext,Task>,也可以是mvc/webapi中某个controller的某个action,所以从抽象的角度讲 一个终结点 就是一个处理请求的委托。由于mvc中action上还有很多attribute,因此我们的终结点还应该提供一个集合,用来存储与此请求处理委托的关联数据。

    一个终结点 = 处理请求的委托 + 与之关联的附加(元)数据

    public void ConfigureServices(IServiceCollection services)
    {
            services.AddControllers();
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                  endpoints.MapControllers();
            });
    }
      • 第一步:执行services.AddControllers()
        Controller的核心服务注册到容器中去
      • 第二步:执行app.UseRouting()
        EndpointRoutingMiddleware中间件注册到http管道中
      • 第三步:执行app.UseAuthorization()
        AuthorizationMiddleware中间件注册到http管道中
      • 第四步:执行app.UseEndpoints(encpoints=>endpoints.MapControllers())
        有两个主要的作用:
        调用endpoints.MapControllers()将本程序集定义的所有ControllerAction转换为一个个的EndPoint放到路由中间件的配置对象RouteOptions
        EndpointMiddleware中间件注册到http管道中
    public static IApplicationBuilder UseRouting(this IApplicationBuilder builder)
    {
           if (builder == null)
           {
                 throw new ArgumentNullException(nameof(builder));
           }
    
           VerifyRoutingServicesAreRegistered(builder);
    
           var endpointRouteBuilder = new DefaultEndpointRouteBuilder(builder);
           builder.Properties[EndpointRouteBuilder] = endpointRouteBuilder;
           
           return builder.UseMiddleware<EndpointRoutingMiddleware>(endpointRouteBuilder);
     }
    app.UseRouting()
    internal sealed class EndpointRoutingMiddleware
        {
            private const string DiagnosticsEndpointMatchedKey = "Microsoft.AspNetCore.Routing.EndpointMatched";
    
            private readonly MatcherFactory _matcherFactory;
            private readonly ILogger _logger;
            private readonly EndpointDataSource _endpointDataSource;
            private readonly DiagnosticListener _diagnosticListener;
            private readonly RequestDelegate _next;
    
            private Task<Matcher> _initializationTask;
    
            public EndpointRoutingMiddleware(
                MatcherFactory matcherFactory,
                ILogger<EndpointRoutingMiddleware> logger,
                IEndpointRouteBuilder endpointRouteBuilder,
                DiagnosticListener diagnosticListener,
                RequestDelegate next)
            {
                if (endpointRouteBuilder == null)
                {
                    throw new ArgumentNullException(nameof(endpointRouteBuilder));
                }
    
                _matcherFactory = matcherFactory ?? throw new ArgumentNullException(nameof(matcherFactory));
                _logger = logger ?? throw new ArgumentNullException(nameof(logger));
                _diagnosticListener = diagnosticListener ?? throw new ArgumentNullException(nameof(diagnosticListener));
                _next = next ?? throw new ArgumentNullException(nameof(next));
    
                _endpointDataSource = new CompositeEndpointDataSource(endpointRouteBuilder.DataSources);
            }
    
            public Task Invoke(HttpContext httpContext)
            {
                // There's already an endpoint, skip maching completely
                var endpoint = httpContext.GetEndpoint();
                if (endpoint != null)
                {
                    Log.MatchSkipped(_logger, endpoint);
                    return _next(httpContext);
                }
    
                // There's an inherent race condition between waiting for init and accessing the matcher
                // this is OK because once `_matcher` is initialized, it will not be set to null again.
                var matcherTask = InitializeAsync();
                if (!matcherTask.IsCompletedSuccessfully)
                {
                    return AwaitMatcher(this, httpContext, matcherTask);
                }
    
                var matchTask = matcherTask.Result.MatchAsync(httpContext);
                if (!matchTask.IsCompletedSuccessfully)
                {
                    return AwaitMatch(this, httpContext, matchTask);
                }
    
                return SetRoutingAndContinue(httpContext);
    
                // Awaited fallbacks for when the Tasks do not synchronously complete
                static async Task AwaitMatcher(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task<Matcher> matcherTask)
                {
                    var matcher = await matcherTask;
                    await matcher.MatchAsync(httpContext);
                    await middleware.SetRoutingAndContinue(httpContext);
                }
    
                static async Task AwaitMatch(EndpointRoutingMiddleware middleware, HttpContext httpContext, Task matchTask)
                {
                    await matchTask;
                    await middleware.SetRoutingAndContinue(httpContext);
                }
    
            }
    
            [MethodImpl(MethodImplOptions.AggressiveInlining)]
            private Task SetRoutingAndContinue(HttpContext httpContext)
            {
                // If there was no mutation of the endpoint then log failure
                var endpoint = httpContext.GetEndpoint();
                if (endpoint == null)
                {
                    Log.MatchFailure(_logger);
                }
                else
                {
                    // Raise an event if the route matched
                    if (_diagnosticListener.IsEnabled() && _diagnosticListener.IsEnabled(DiagnosticsEndpointMatchedKey))
                    {
                        // We're just going to send the HttpContext since it has all of the relevant information
                        _diagnosticListener.Write(DiagnosticsEndpointMatchedKey, httpContext);
                    }
    
                    Log.MatchSuccess(_logger, endpoint);
                }
    
                return _next(httpContext);
            }
    
            // Initialization is async to avoid blocking threads while reflection and things
            // of that nature take place.
            //
            // We've seen cases where startup is very slow if we  allow multiple threads to race
            // while initializing the set of endpoints/routes. Doing CPU intensive work is a
            // blocking operation if you have a low core count and enough work to do.
            private Task<Matcher> InitializeAsync()
            {
                var initializationTask = _initializationTask;
                if (initializationTask != null)
                {
                    return initializationTask;
                }
    
                return InitializeCoreAsync();
            }
    
            private Task<Matcher> InitializeCoreAsync()
            {
                var initialization = new TaskCompletionSource<Matcher>(TaskCreationOptions.RunContinuationsAsynchronously);
                var initializationTask = Interlocked.CompareExchange(ref _initializationTask, initialization.Task, null);
                if (initializationTask != null)
                {
                    // This thread lost the race, join the existing task.
                    return initializationTask;
                }
    
                // This thread won the race, do the initialization.
                try
                {
                    var matcher = _matcherFactory.CreateMatcher(_endpointDataSource);
    
                    // Now replace the initialization task with one created with the default execution context.
                    // This is important because capturing the execution context will leak memory in ASP.NET Core.
                    using (ExecutionContext.SuppressFlow())
                    {
                        _initializationTask = Task.FromResult(matcher);
                    }
    
                    // Complete the task, this will unblock any requests that came in while initializing.
                    initialization.SetResult(matcher);
                    return initialization.Task;
                }
                catch (Exception ex)
                {
                    // Allow initialization to occur again. Since DataSources can change, it's possible
                    // for the developer to correct the data causing the failure.
                    _initializationTask = null;
    
                    // Complete the task, this will throw for any requests that came in while initializing.
                    initialization.SetException(ex);
                    return initialization.Task;
                }
            }
    
            private static class Log
            {
                private static readonly Action<ILogger, string, Exception> _matchSuccess = LoggerMessage.Define<string>(
                    LogLevel.Debug,
                    new EventId(1, "MatchSuccess"),
                    "Request matched endpoint '{EndpointName}'");
    
                private static readonly Action<ILogger, Exception> _matchFailure = LoggerMessage.Define(
                    LogLevel.Debug,
                    new EventId(2, "MatchFailure"),
                    "Request did not match any endpoints");
    
                private static readonly Action<ILogger, string, Exception> _matchingSkipped = LoggerMessage.Define<string>(
                    LogLevel.Debug,
                    new EventId(3, "MatchingSkipped"),
                    "Endpoint '{EndpointName}' already set, skipping route matching.");
    
                public static void MatchSuccess(ILogger logger, Endpoint endpoint)
                {
                    _matchSuccess(logger, endpoint.DisplayName, null);
                }
    
                public static void MatchFailure(ILogger logger)
                {
                    _matchFailure(logger, null);
                }
    
                public static void MatchSkipped(ILogger logger, Endpoint endpoint)
                {
                    _matchingSkipped(logger, endpoint.DisplayName, null);
                }
            }
        }
    EndpointRoutingMiddleware

    我们从它的源码中可以看到,EndpointRoutingMiddleware中间件先是创建matcher,然后调用matcher.MatchAsync(httpContext)去寻找Endpoint,最后通过httpContext.GetEndpoint()验证了是否已经匹配到了正确的Endpoint并交个下个中间件继续执行!

    public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
    {
           if (builder == null)
           {
                  throw new ArgumentNullException(nameof(builder));
           }
    
           if (configure == null)
           {
                  throw new ArgumentNullException(nameof(configure));
           }
    
           VerifyRoutingServicesAreRegistered(builder);
    
           VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder);
    
           configure(endpointRouteBuilder);
    
           // Yes, this mutates an IOptions. We're registering data sources in a global collection which
           // can be used for discovery of endpoints or URL generation.
           //
           // Each middleware gets its own collection of data sources, and all of those data sources also
           // get added to a global collection.
           var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
            foreach (var dataSource in endpointRouteBuilder.DataSources)
            {
                  routeOptions.Value.EndpointDataSources.Add(dataSource);
            }
    
            return builder.UseMiddleware<EndpointMiddleware>();
    }
    
    internal class DefaultEndpointRouteBuilder : IEndpointRouteBuilder
    {
            public DefaultEndpointRouteBuilder(IApplicationBuilder applicationBuilder)
            {
                ApplicationBuilder = applicationBuilder ?? throw new ArgumentNullException(nameof(applicationBuilder));
                DataSources = new List<EndpointDataSource>();
            }
    
            public IApplicationBuilder ApplicationBuilder { get; }
    
            public IApplicationBuilder CreateApplicationBuilder() => ApplicationBuilder.New();
    
            public ICollection<EndpointDataSource> DataSources { get; }
    
            public IServiceProvider ServiceProvider => ApplicationBuilder.ApplicationServices;
        }
    app.UseEndpoints()

    代码中构建了DefaultEndpointRouteBuilder 终结点路由构建者对象,该对象中存储了Endpoint的集合数据;同时把终结者路由集合数据存储在了routeOptions 中,并注册了EndpointMiddleware 中间件到http管道中;

    Endpoint对象代码如下:

    /// <summary>
    /// Represents a logical endpoint in an application.
    /// </summary>
    public class Endpoint
    {
            /// <summary>
            /// Creates a new instance of <see cref="Endpoint"/>.
            /// </summary>
            /// <param name="requestDelegate">The delegate used to process requests for the endpoint.</param>
            /// <param name="metadata">
            /// The endpoint <see cref="EndpointMetadataCollection"/>. May be null.
            /// </param>
            /// <param name="displayName">
            /// The informational display name of the endpoint. May be null.
            /// </param>
            public Endpoint(
                RequestDelegate requestDelegate,
                EndpointMetadataCollection metadata,
                string displayName)
            {
                // All are allowed to be null
                RequestDelegate = requestDelegate;
                Metadata = metadata ?? EndpointMetadataCollection.Empty;
                DisplayName = displayName;
            }
    
            /// <summary>
            /// Gets the informational display name of this endpoint.
            /// </summary>
            public string DisplayName { get; }
    
            /// <summary>
            /// Gets the collection of metadata associated with this endpoint.
            /// </summary>
            public EndpointMetadataCollection Metadata { get; }
    
            /// <summary>
            /// Gets the delegate used to process requests for the endpoint.
            /// </summary>
            public RequestDelegate RequestDelegate { get; }
    
            public override string ToString() => DisplayName ?? base.ToString();
        }
    Endpoint

    Endpoint 对象代码中有两个关键类型属性分别是EndpointMetadataCollection 类型和RequestDelegate

    • EndpointMetadataCollection:存储了Controller 和Action相关的元素集合,包含Action 上的Attribute 特性数据等
    • RequestDelegate :存储了Action 也即委托,这里是每一个Controller 的Action 方法

    再回过头来看看EndpointMiddleware 中间件和核心代码,EndpointMiddleware 的一大核心代码主要是执行Endpoint 的RequestDelegate 委托,也即Controller 中的Action 的执行。

    public Task Invoke(HttpContext httpContext)
    {
            var endpoint = httpContext.GetEndpoint();
            if (endpoint?.RequestDelegate != null)
            {
                 if (!_routeOptions.SuppressCheckForUnhandledSecurityMetadata)
                 {
                     if (endpoint.Metadata.GetMetadata<IAuthorizeData>() != null &&
                            !httpContext.Items.ContainsKey(AuthorizationMiddlewareInvokedKey))
                      {
                          ThrowMissingAuthMiddlewareException(endpoint);
                      }
    
                      if (endpoint.Metadata.GetMetadata<ICorsMetadata>() != null &&
                           !httpContext.Items.ContainsKey(CorsMiddlewareInvokedKey))
                       {
                           ThrowMissingCorsMiddlewareException(endpoint);
                       }
                 }
    
                Log.ExecutingEndpoint(_logger, endpoint);
    
                try
                {
                     var requestTask = endpoint.RequestDelegate(httpContext);
                     if (!requestTask.IsCompletedSuccessfully)
                     {
                         return AwaitRequestTask(endpoint, requestTask, _logger);
                     }
                }
                catch (Exception exception)
                {
                     Log.ExecutedEndpoint(_logger, endpoint);
                     return Task.FromException(exception);
                }
    
                Log.ExecutedEndpoint(_logger, endpoint);
                return Task.CompletedTask;
            }
    
            return _next(httpContext);
    
            static async Task AwaitRequestTask(Endpoint endpoint, Task requestTask, ILogger logger)
             {
                 try
                 {
                     await requestTask;
                 }
                 finally
                 {
                     Log.ExecutedEndpoint(logger, endpoint);
                 }
             }
    }
    View Code
    1. 当访问一个Web 应用地址时,Asp.Net Core 是怎么执行到Controller 的Action的呢?

    答:程序启动的时候会把所有的Controller 中的Action 映射存储到routeOptions 的集合中,Action 映射成Endpoint终结者 的RequestDelegate 委托属性,最后通过UseEndPoints 添加EndpointMiddleware 中间件进行执行,同时这个中间件中的Endpoint 终结者路由已经是通过Routing匹配后的路由。

    2. EndPoint 跟普通路由又存在着什么样的关系?

    答:Ednpoint 终结者路由是普通路由map 转换后的委托路由,里面包含了路由方法的所有元素信息EndpointMetadataCollection 和RequestDelegate 委托。

    3. UseRouting() 、UseAuthorization()UseEndpoints() 这三个中间件的关系是什么呢?

    答:UseRouing 中间件主要是路由匹配,找到匹配的终结者路由Endpoint ;UseEndpoints 中间件主要针对UseRouting 中间件匹配到的路由进行 委托方法的执行等操作。
    UseAuthorization 中间件主要针对 UseRouting 中间件中匹配到的路由进行拦截 做授权验证操作等,通过则执行下一个中间件UseEndpoints(),具体的关系可以看下面的流程图:

     

    上面流程图中省略了一些部分,主要是把UseRouting 、UseAuthorization 、UseEndpoint 这三个中间件的关系突显出来。

    最后我们可以在UseRouting() 和UseEndpoint() 注册的Http 管道之间 注册其他牛逼的自定义中间件,以实现我们自己都有的业务逻辑

    路由

    url和控制器方法映射

    两个作用

    1、根据url对应到action上

    2、根据action生成url

    一进一出

    注册方式

    1、 路由模板

    适合MVC页面

    2、RouteAttribute方式

     适合webapi

    路由约束

    类型约束

    范围约束

    正则表达式

    是否必选

    自定义路由

    URL生成

    LinkGenerator(可以通过容器获的)

    IUrlHelper 与MVC框架里的HtmlHelper很像

    注意细节

    1、action中参数[FromServices]LinkGenerator linkGenerator,还有FromRoute     FromBody   FromForm   FromHeader   FromQuery

    2、ObsoleteAttribute隔版本废弃

    3、将API 约束在特定目录下,如/api/ (可与功能性页面隔离)

    4、约定好API 的表达契约 

    5、Restful 不是必须的

    应用启动

    Startup 

    泛型主机

    下面服务只能通过构造函数注入

    public class Startup
    {
        private readonly IWebHostEnvironment _env;
    
        public Startup(IConfiguration configuration, IWebHostEnvironment env)
        {
            Configuration = configuration;
            _env = env;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            if (_env.IsDevelopment())
            {
            }
            else
            {
            }
        }
    }

    ConfigureServices 方法

    • 可选。
    • 在 Configure 方法配置应用服务之前,由主机调用。
    • 其中按常规设置配置选项

    主机可能会在调用 Startup 方法之前配置某些服务。 有关详细信息,请参阅主机

    对于需要大量设置的功能,Add{Service} 上有 IServiceCollection 扩展方法。 例如,AddDbContext、AddDefaultIdentity、AddEntityFrameworkStores 和 AddRazorPages:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
    
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));
            services.AddDefaultIdentity<IdentityUser>(
                options => options.SignIn.RequireConfirmedAccount = true)
                .AddEntityFrameworkStores<ApplicationDbContext>();
    
            services.AddRazorPages();
        }

    将服务添加到服务容器,使其在应用和 Configure 方法中可用。 服务通过依赖关系注入或 ApplicationServices 进行解析。

    Configure 方法

    Configure 方法用于指定应用响应 HTTP 请求的方式。 可通过将中间件组件添加到 IApplicationBuilder 实例来配置请求管道。 IApplicationBuilder 方法可使用 Configure,但未在服务容器中注册。 托管创建 IApplicationBuilder 并将其直接传递到 Configure

    ASP.NET Core 模板配置的管道支持:

    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }
    
        public IConfiguration Configuration { get; }
    
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
        }
    
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }
    //重定向
            app.UseHttpsRedirection();
    //静态文件
            app.UseStaticFiles();
    
            app.UseRouting();
    
            app.UseAuthorization();
    
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }

    请求管道中的每个中间件组件负责调用管道中的下一个组件,或在适当情况下使链发生短路。

    可以在 Configure方法签名中指定其他服务,如 ILoggerFactoryConfigureServices 或 IWebHostEnvironment 中定义的任何内容。 如果这些服务可用,则会被注入。

    有关如何使用 IApplicationBuilder 和中间件处理顺序的详细信息,请参阅 ASP.NET Core 中间件

    在不启动的情况下配置服务

    若要配置服务和请求处理管道,而不使用 Startup 类,请在主机生成器上调用 ConfigureServices 和 Configure 便捷方法。 多次调用 ConfigureServices 将追加到另一个。 如果存在多个 Configure 方法调用,则使用最后一个 Configure 调用。

    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }
    
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureAppConfiguration((hostingContext, config) =>
                {
                })
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.ConfigureServices(services =>
                    {
                        services.AddControllersWithViews();
                    })
                    .Configure(app =>
                    {
                        var loggerFactory = app.ApplicationServices
                            .GetRequiredService<ILoggerFactory>();
                        var logger = loggerFactory.CreateLogger<Program>();
                        var env = app.ApplicationServices.GetRequiredService<IWebHostEnvironment>();
                        var config = app.ApplicationServices.GetRequiredService<IConfiguration>();
    
                        logger.LogInformation("Logged in Configure");
    
                        if (env.IsDevelopment())
                        {
                            app.UseDeveloperExceptionPage();
                        }
                        else
                        {
                            app.UseExceptionHandler("/Home/Error");
                            app.UseHsts();
                        }
    
                        var configValue = config["MyConfigKey"];
                    });
                });
            });
    }
    View Code
  • 相关阅读:
    GIT学习笔记(2):时光机穿梭与远程仓库
    CNN学习笔记:正则化缓解过拟合
    Java分布式:RPC(远程过程调用)
    设计模式:学习笔记(12)——代理模式
    算法:乐观锁与悲观锁
    Python:笔记(5)——错误、调试和测试
    算法:LRU(最近最少使用)
    Python:笔记(4)——高级特性
    方法论:带着问题找答案
    Cache-Aside模式
  • 原文地址:https://www.cnblogs.com/wudequn/p/12833869.html
Copyright © 2011-2022 走看看