1. Changes in DI container

a. Specified RequireHttpsPermanent and SSL port MVC pipeline options
    b. Configured HttpsRedirection
2. Used HttpsRedirection MiddleWare
This commit is contained in:
rafsanulhasan 2018-09-01 16:49:39 +06:00
parent a62648bac9
commit 0d63172dc8
3 changed files with 278 additions and 278 deletions

View File

@ -1,184 +1,178 @@
using Autofac; using Microsoft.eShopOnContainers.Services.Identity.API.Certificates;
using Autofac.Extensions.DependencyInjection;
using IdentityServer4.Services;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.ServiceFabric;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopOnContainers.Services.Identity.API.Certificates;
using Microsoft.eShopOnContainers.Services.Identity.API.Data; using Microsoft.eShopOnContainers.Services.Identity.API.Data;
using Microsoft.eShopOnContainers.Services.Identity.API.Models; using Microsoft.eShopOnContainers.Services.Identity.API.Models;
using Microsoft.eShopOnContainers.Services.Identity.API.Services; using Microsoft.eShopOnContainers.Services.Identity.API.Services;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.HealthChecks;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
using System;
using System.Reflection;
namespace Microsoft.eShopOnContainers.Services.Identity.API namespace Microsoft.eShopOnContainers.Services.Identity.API
{ {
public class Startup public class Startup
{ {
public Startup(IConfiguration configuration) public Startup(IConfiguration configuration)
{ {
Configuration = configuration; Configuration = configuration;
} }
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
public IServiceProvider ConfigureServices(IServiceCollection services) public IServiceProvider ConfigureServices(IServiceCollection services)
{ {
RegisterAppInsights(services); RegisterAppInsights(services);
// Add framework services. // Add framework services.
services.AddDbContext<ApplicationDbContext>(options => services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration["ConnectionString"], options.UseSqlServer(Configuration["ConnectionString"],
sqlServerOptionsAction: sqlOptions => sqlServerOptionsAction: sqlOptions =>
{ {
sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name); sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency //Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
})); }));
services.AddIdentity<ApplicationUser, IdentityRole>() services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>() .AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders(); .AddDefaultTokenProviders();
services.Configure<AppSettings>(Configuration); services.Configure<AppSettings>(Configuration);
services.AddMvc(); services.AddMvc(opts =>
{
opts.SslPort = 4105;
opts.RequireHttpsPermanent = true;
});
if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString) services.AddHttpsRedirection(opts =>
{ {
services.AddDataProtection(opts => opts.HttpsPort = 4105;
{ });
opts.ApplicationDiscriminator = "eshop.identity";
})
.PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys");
}
services.AddHealthChecks(checks => if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString)
{ {
var minutes = 1; services.AddDataProtection(opts =>
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) {
{ opts.ApplicationDiscriminator = "eshop.identity";
minutes = minutesParsed; })
} .PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys");
checks.AddSqlCheck("Identity_Db", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes)); }
});
services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>(); services.AddHealthChecks(checks =>
services.AddTransient<IRedirectService, RedirectService>(); {
var minutes = 1;
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
{
minutes = minutesParsed;
}
checks.AddSqlCheck("Identity_Db", Configuration["ConnectionString"], TimeSpan.FromMinutes(minutes));
});
var connectionString = Configuration["ConnectionString"]; services.AddTransient<ILoginService<ApplicationUser>, EFLoginService>();
var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; services.AddTransient<IRedirectService, RedirectService>();
// Adds IdentityServer var connectionString = Configuration["ConnectionString"];
services.AddIdentityServer(x => x.IssuerUri = "null") var migrationsAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
.AddSigningCredential(Certificate.Get())
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
})
.Services.AddTransient<IProfileService, ProfileService>();
var container = new ContainerBuilder(); // Adds IdentityServer
container.Populate(services); services.AddIdentityServer(x => x.IssuerUri = "null")
.AddSigningCredential(Certificate.Get())
.AddAspNetIdentity<ApplicationUser>()
.AddConfigurationStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
})
.AddOperationalStore(options =>
{
options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString,
sqlServerOptionsAction: sqlOptions =>
{
sqlOptions.MigrationsAssembly(migrationsAssembly);
//Configuring Connection Resiliency: https://docs.microsoft.com/en-us/ef/core/miscellaneous/connection-resiliency
sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
});
})
.Services.AddTransient<IProfileService, ProfileService>();
return new AutofacServiceProvider(container.Build()); var container = new ContainerBuilder();
} container.Populate(services);
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. return new AutofacServiceProvider(container.Build());
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) }
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddAzureWebAppDiagnostics();
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
if (env.IsDevelopment()) // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
{ public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
app.UseDeveloperExceptionPage(); {
app.UseDatabaseErrorPage(); loggerFactory.AddConsole(Configuration.GetSection("Logging"));
} loggerFactory.AddDebug();
else loggerFactory.AddAzureWebAppDiagnostics();
{ loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
app.UseExceptionHandler("/Home/Error");
}
var pathBase = Configuration["PATH_BASE"]; if (env.IsDevelopment())
if (!string.IsNullOrEmpty(pathBase)) {
{ app.UseDeveloperExceptionPage();
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); app.UseDatabaseErrorPage();
app.UsePathBase(pathBase); }
} else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseHttpsRedirection();
var pathBase = Configuration["PATH_BASE"];
if (!string.IsNullOrEmpty(pathBase))
{
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'");
app.UsePathBase(pathBase);
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
app.UseStaticFiles(); app.UseStaticFiles();
// Make work identity server redirections in Edge and lastest versions of browers. WARN: Not valid in a production environment. // Make work identity server redirections in Edge and lastest versions of browers. WARN: Not valid in a production environment.
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
context.Response.Headers.Add("Content-Security-Policy", "script-src 'unsafe-inline'"); context.Response.Headers.Add("Content-Security-Policy", "script-src 'unsafe-inline'");
await next(); await next();
}); });
app.UseForwardedHeaders(); app.UseForwardedHeaders();
// Adds IdentityServer // Adds IdentityServer
app.UseIdentityServer(); app.UseIdentityServer();
app.UseMvc(routes => app.UseMvc(routes =>
{ {
routes.MapRoute( routes.MapRoute(
name: "default", name: "default",
template: "{controller=Home}/{action=Index}/{id?}"); template: "{controller=Home}/{action=Index}/{id?}");
}); });
} }
private void RegisterAppInsights(IServiceCollection services) private void RegisterAppInsights(IServiceCollection services)
{ {
services.AddApplicationInsightsTelemetry(Configuration); services.AddApplicationInsightsTelemetry(Configuration);
var orchestratorType = Configuration.GetValue<string>("OrchestratorType"); var orchestratorType = Configuration.GetValue<string>("OrchestratorType");
if (orchestratorType?.ToUpper() == "K8S") if (orchestratorType?.ToUpper() == "K8S")
{ {
// Enable K8s telemetry initializer // Enable K8s telemetry initializer
services.EnableKubernetes(); services.EnableKubernetes();
} }
if (orchestratorType?.ToUpper() == "SF") if (orchestratorType?.ToUpper() == "SF")
{ {
// Enable SF telemetry initializer // Enable SF telemetry initializer
services.AddSingleton<ITelemetryInitializer>((serviceProvider) => services.AddSingleton<ITelemetryInitializer>((serviceProvider) =>
new FabricTelemetryInitializer()); new FabricTelemetryInitializer());
} }
} }
} }
} }

View File

@ -41,6 +41,10 @@ namespace Microsoft.eShopOnContainers.WebMVC
opts.CheckConsentNeeded = context => true; opts.CheckConsentNeeded = context => true;
opts.MinimumSameSitePolicy = SameSiteMode.None; opts.MinimumSameSitePolicy = SameSiteMode.None;
}); });
services.AddHttpsRedirection(opts=>
{
opts.HttpsPort = 4100;
});
services.AddAppInsight(Configuration) services.AddAppInsight(Configuration)
.AddHealthChecks(Configuration) .AddHealthChecks(Configuration)
.AddCustomMvc(Configuration) .AddCustomMvc(Configuration)
@ -73,7 +77,7 @@ namespace Microsoft.eShopOnContainers.WebMVC
app.UsePathBase(pathBase); app.UsePathBase(pathBase);
} }
app.UseHttpsRedirection();
app.UseCookiePolicy(); app.UseCookiePolicy();
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
@ -156,7 +160,11 @@ namespace Microsoft.eShopOnContainers.WebMVC
services.AddOptions(); services.AddOptions();
services.Configure<AppSettings>(configuration); services.Configure<AppSettings>(configuration);
services.AddMvc(); services.AddMvc(opts=>
{
opts.SslPort = 4100;
opts.RequireHttpsPermanent = true;
});
services.AddSession(); services.AddSession();

View File

@ -1,158 +1,156 @@
using eShopOnContainers.WebSPA; using eShopOnContainers.WebSPA;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.ServiceFabric;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.HealthChecks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json.Serialization;
using StackExchange.Redis;
using System;
using System.IO;
using WebSPA.Infrastructure; using WebSPA.Infrastructure;
namespace eShopConContainers.WebSPA namespace eShopConContainers.WebSPA
{ {
public class Startup public class Startup
{ {
public Startup(IConfiguration configuration) public Startup(IConfiguration configuration)
{ {
Configuration = configuration; Configuration = configuration;
} }
public IConfiguration Configuration { get; } public IConfiguration Configuration { get; }
private IHostingEnvironment _hostingEnv; private readonly IHostingEnvironment _hostingEnv;
public Startup(IHostingEnvironment env) public Startup(IHostingEnvironment env)
{ {
_hostingEnv = env; _hostingEnv = env;
var localPath = new Uri(Configuration["ASPNETCORE_URLS"])?.LocalPath ?? "/"; var localPath = new Uri(Configuration["ASPNETCORE_URLS"])?.LocalPath ?? "/";
Configuration["BaseUrl"] = localPath; Configuration["BaseUrl"] = localPath;
} }
// This method gets called by the runtime. Use this method to add services to the container. // This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940 // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services) public void ConfigureServices(IServiceCollection services)
{ {
RegisterAppInsights(services); RegisterAppInsights(services);
services.AddHealthChecks(checks => services.AddHealthChecks(checks =>
{ {
var minutes = 1; var minutes = 1;
if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed)) if (int.TryParse(Configuration["HealthCheck:Timeout"], out var minutesParsed))
{ {
minutes = minutesParsed; minutes = minutesParsed;
} }
checks.AddUrlCheck(Configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes)); checks.AddUrlCheck(Configuration["CatalogUrlHC"], TimeSpan.FromMinutes(minutes));
checks.AddUrlCheck(Configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes)); checks.AddUrlCheck(Configuration["OrderingUrlHC"], TimeSpan.FromMinutes(minutes));
checks.AddUrlCheck(Configuration["BasketUrlHC"], TimeSpan.Zero); //No cache for this HealthCheck, better just for demos checks.AddUrlCheck(Configuration["BasketUrlHC"], TimeSpan.Zero); //No cache for this HealthCheck, better just for demos
checks.AddUrlCheck(Configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes)); checks.AddUrlCheck(Configuration["IdentityUrlHC"], TimeSpan.FromMinutes(minutes));
checks.AddUrlCheck(Configuration["MarketingUrlHC"], TimeSpan.FromMinutes(minutes)); checks.AddUrlCheck(Configuration["MarketingUrlHC"], TimeSpan.FromMinutes(minutes));
}); });
services.Configure<AppSettings>(Configuration); services.Configure<AppSettings>(Configuration);
if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString) if (Configuration.GetValue<string>("IsClusterEnv") == bool.TrueString)
{ {
services.AddDataProtection(opts => services.AddDataProtection(opts =>
{ {
opts.ApplicationDiscriminator = "eshop.webspa"; opts.ApplicationDiscriminator = "eshop.webspa";
}) })
.PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys"); .PersistKeysToRedis(ConnectionMultiplexer.Connect(Configuration["DPConnectionString"]), "DataProtection-Keys");
} }
services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN"); services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");
services.AddMvc() services
.AddJsonOptions(options => .AddMvc(opts =>
{ {
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); opts.SslPort = 4104;
}); opts.RequireHttpsPermanent = true;
} })
.AddJsonOptions(options =>
{
options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
});
services.AddHttpsRedirection(opts =>
{
opts.HttpsPort = 4104;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery) public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery)
{ {
loggerFactory.AddAzureWebAppDiagnostics(); loggerFactory.AddAzureWebAppDiagnostics();
loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace); loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Trace);
if (env.IsDevelopment()) if (env.IsDevelopment())
{ {
app.UseDeveloperExceptionPage(); app.UseDeveloperExceptionPage();
} }
// Configure XSRF middleware, This pattern is for SPA style applications where XSRF token is added on Index page app.UseHttpsRedirection();
// load and passed back token on every subsequent async request
// app.Use(async (context, next) =>
// {
// if (string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase))
// {
// var tokens = antiforgery.GetAndStoreTokens(context);
// context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
// }
// await next.Invoke();
// });
//Seed Data // Configure XSRF middleware, This pattern is for SPA style applications where XSRF token is added on Index page
WebContextSeed.Seed(app, env, loggerFactory); // load and passed back token on every subsequent async request
// app.Use(async (context, next) =>
// {
// if (string.Equals(context.Request.Path.Value, "/", StringComparison.OrdinalIgnoreCase))
// {
// var tokens = antiforgery.GetAndStoreTokens(context);
// context.Response.Cookies.Append("XSRF-TOKEN", tokens.RequestToken, new CookieOptions() { HttpOnly = false });
// }
// await next.Invoke();
// });
var pathBase = Configuration["PATH_BASE"]; //Seed Data
if (!string.IsNullOrEmpty(pathBase)) WebContextSeed.Seed(app, env, loggerFactory);
{
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'"); var pathBase = Configuration["PATH_BASE"];
app.UsePathBase(pathBase); if (!string.IsNullOrEmpty(pathBase))
} {
loggerFactory.CreateLogger("init").LogDebug($"Using PATH BASE '{pathBase}'");
app.UsePathBase(pathBase);
}
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200)); app.Map("/liveness", lapp => lapp.Run(async ctx => ctx.Response.StatusCode = 200));
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
app.Use(async (context, next) => app.Use(async (context, next) =>
{ {
await next(); await next();
// If there's no available file and the request doesn't contain an extension, we're probably trying to access a page. // If there's no available file and the request doesn't contain an extension, we're probably trying to access a page.
// Rewrite request to use app root // Rewrite request to use app root
if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value) && !context.Request.Path.Value.StartsWith("/api")) if (context.Response.StatusCode == 404 && !Path.HasExtension(context.Request.Path.Value) && !context.Request.Path.Value.StartsWith("/api"))
{ {
context.Request.Path = "/index.html"; context.Request.Path = "/index.html";
context.Response.StatusCode = 200; // Make sure we update the status code, otherwise it returns 404 context.Response.StatusCode = 200; // Make sure we update the status code, otherwise it returns 404
await next(); await next();
} }
}); });
app.UseDefaultFiles(); app.UseDefaultFiles();
app.UseStaticFiles(); app.UseStaticFiles();
app.UseMvcWithDefaultRoute(); app.UseMvcWithDefaultRoute();
} }
private void RegisterAppInsights(IServiceCollection services) private void RegisterAppInsights(IServiceCollection services)
{ {
services.AddApplicationInsightsTelemetry(Configuration); services.AddApplicationInsightsTelemetry(Configuration);
var orchestratorType = Configuration.GetValue<string>("OrchestratorType"); var orchestratorType = Configuration.GetValue<string>("OrchestratorType");
if (orchestratorType?.ToUpper() == "K8S") if (orchestratorType?.ToUpper() == "K8S")
{ {
// Enable K8s telemetry initializer // Enable K8s telemetry initializer
services.EnableKubernetes(); services.EnableKubernetes();
} }
if (orchestratorType?.ToUpper() == "SF") if (orchestratorType?.ToUpper() == "SF")
{ {
// Enable SF telemetry initializer // Enable SF telemetry initializer
services.AddSingleton<ITelemetryInitializer>((serviceProvider) => services.AddSingleton<ITelemetryInitializer>((serviceProvider) =>
new FabricTelemetryInitializer()); new FabricTelemetryInitializer());
} }
} }
} }
} }