diff --git a/codecov.yml b/codecov.yml index bc179cef5..553f2235e 100644 --- a/codecov.yml +++ b/codecov.yml @@ -5,6 +5,7 @@ coverage: target: auto # auto compares coverage to the previous base commit ignore: - "**/Startup.cs" + - "**/Startup/*.cs" - "**/Program.cs" - "**/PortalDbContext.cs" - "**/Migrations/*.cs" diff --git a/src/AzureIoTHub.Portal.Application/AzureIoTHub.Portal.Application.csproj b/src/AzureIoTHub.Portal.Application/AzureIoTHub.Portal.Application.csproj index b25d8e162..a67dc9917 100644 --- a/src/AzureIoTHub.Portal.Application/AzureIoTHub.Portal.Application.csproj +++ b/src/AzureIoTHub.Portal.Application/AzureIoTHub.Portal.Application.csproj @@ -6,18 +6,11 @@ enable - - - - - - - - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AzureIoTHub.Portal.Application/Startup/IServiceCollectionExtension.cs b/src/AzureIoTHub.Portal.Application/Startup/IServiceCollectionExtension.cs new file mode 100644 index 000000000..a0ce6274d --- /dev/null +++ b/src/AzureIoTHub.Portal.Application/Startup/IServiceCollectionExtension.cs @@ -0,0 +1,20 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Application.Startup +{ + using Microsoft.Extensions.DependencyInjection; + + public static class IServiceCollectionExtension + { + public static IServiceCollection AddApplicationLayer(this IServiceCollection services) + { + return services.ConfigureMappingProfiles(); + } + + private static IServiceCollection ConfigureMappingProfiles(this IServiceCollection services) + { + return services.AddAutoMapper(typeof(IServiceCollectionExtension)); + } + } +} diff --git a/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj b/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj index 60b7f39a6..f864bd5b0 100644 --- a/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj +++ b/src/AzureIoTHub.Portal.Infrastructure/AzureIoTHub.Portal.Infrastructure.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -7,6 +7,7 @@ + @@ -90,6 +91,7 @@ + @@ -125,9 +127,14 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + @@ -137,4 +144,4 @@ - + \ No newline at end of file diff --git a/src/AzureIoTHub.Portal.Application/Resources/default-template-icon.png b/src/AzureIoTHub.Portal.Infrastructure/Resources/default-template-icon.png similarity index 100% rename from src/AzureIoTHub.Portal.Application/Resources/default-template-icon.png rename to src/AzureIoTHub.Portal.Infrastructure/Resources/default-template-icon.png diff --git a/src/AzureIoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs b/src/AzureIoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs new file mode 100644 index 000000000..c08282e96 --- /dev/null +++ b/src/AzureIoTHub.Portal.Infrastructure/Startup/IServiceCollectionExtension.cs @@ -0,0 +1,185 @@ +// Copyright (c) CGI France. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace AzureIoTHub.Portal.Infrastructure.Startup +{ + using System.Net; + using Azure.Storage.Blobs; + using AzureIoTHub.Portal.Application.Providers; + using AzureIoTHub.Portal.Application.Services; + using AzureIoTHub.Portal.Application.Wrappers; + using AzureIoTHub.Portal.Domain; + using AzureIoTHub.Portal.Domain.Options; + using AzureIoTHub.Portal.Infrastructure.Providers; + using AzureIoTHub.Portal.Infrastructure.Services; + using AzureIoTHub.Portal.Infrastructure.ServicesHealthCheck; + using AzureIoTHub.Portal.Infrastructure.Wrappers; + using EntityFramework.Exceptions.PostgreSQL; + using Microsoft.Azure.Devices.Provisioning.Service; + using Microsoft.Azure.Devices; + using Microsoft.EntityFrameworkCore; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Polly; + using Polly.Extensions.Http; + using AzureIoTHub.Portal.Domain.Repositories; + using AzureIoTHub.Portal.Infrastructure.Repositories; + using AzureIoTHub.Portal.Application.Mappers; + using AzureIoTHub.Portal.Infrastructure.Mappers; + using AzureIoTHub.Portal.Models.v10.LoRaWAN; + using AzureIoTHub.Portal.Models.v10; + using AzureIoTHub.Portal.Application.Managers; + using AzureIoTHub.Portal.Infrastructure.Managers; + using Azure.Storage.Blobs.Models; + using Microsoft.Extensions.Configuration; + + public static class IServiceCollectionExtension + { + public static IServiceCollection AddInfrastructureLayer(this IServiceCollection services, ConfigHandler configuration) + { + return services.ConfigureDatabase(configuration) + .ConfigureRepositories() + .ConfigureImageBlobStorage(configuration) + .AddLoRaWanSupport(configuration) + .ConfigureDeviceRegstryDependencies(configuration) + .ConfigureServices() + .ConfigureMappers() + .ConfigureHealthCheck(); + } + + private static IServiceCollection AddLoRaWanSupport(this IServiceCollection services, ConfigHandler configuration) + { + _ = services.Configure(opts => + { + opts.Enabled = configuration.IsLoRaEnabled; + opts.KeyManagementApiVersion = configuration.LoRaKeyManagementApiVersion; + opts.KeyManagementCode = configuration.LoRaKeyManagementCode; + opts.KeyManagementUrl = configuration.LoRaKeyManagementUrl; + }); + + if (!configuration.IsLoRaEnabled) + { + return services; + } + + var transientHttpErrorPolicy = HttpPolicyExtensions + .HandleTransientHttpError() + .OrResult(c => c.StatusCode == HttpStatusCode.NotFound) + .WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(100)); + + _ = services.AddHttpClient("RestClient") + .AddPolicyHandler(transientHttpErrorPolicy); + + _ = services.AddHttpClient((sp, client) => + { + var opts = sp.GetService>().Value; + + client.BaseAddress = new Uri(opts.KeyManagementUrl); + client.DefaultRequestHeaders.Add("x-functions-key", opts.KeyManagementCode); + client.DefaultRequestHeaders.Add("api-version", opts.KeyManagementApiVersion ?? "2022-03-04"); + }) + .AddPolicyHandler(transientHttpErrorPolicy); + + return services; + } + + private static IServiceCollection ConfigureDeviceRegstryDependencies(this IServiceCollection services, ConfigHandler configuration) + { + _ = services.AddTransient(); + _ = services.AddTransient(); + + _ = services.AddScoped(_ => RegistryManager.CreateFromConnectionString(configuration.IoTHubConnectionString)); + _ = services.AddScoped(_ => ServiceClient.CreateFromConnectionString(configuration.IoTHubConnectionString)); + _ = services.AddScoped(_ => ProvisioningServiceClient.CreateFromConnectionString(configuration.DPSConnectionString)); + + return services; + } + + private static IServiceCollection ConfigureDatabase(this IServiceCollection services, ConfigHandler configuration) + { + _ = services + .AddDbContextPool(opts => + { + _ = opts.UseNpgsql(configuration.PostgreSQLConnectionString); + _ = opts.UseExceptionProcessor(); + }); + + if (string.IsNullOrEmpty(configuration.PostgreSQLConnectionString)) + return services; + + _ = services.AddScoped>(); + + var dbContextOptions = new DbContextOptionsBuilder(); + _ = dbContextOptions.UseNpgsql(configuration.PostgreSQLConnectionString); + + using var ctx = new PortalDbContext(dbContextOptions.Options); + ctx.Database.Migrate(); + + return services; + } + + private static IServiceCollection ConfigureRepositories(this IServiceCollection services) + { + return services.AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped() + .AddScoped(); + } + + private static IServiceCollection ConfigureMappers(this IServiceCollection services) + { + _ = services.AddTransient(); + _ = services.AddTransient(); + _ = services.AddTransient(); + + return services.AddTransient, DeviceTwinMapper>() + .AddTransient, LoRaDeviceTwinMapper>() + .AddTransient, DeviceModelMapper>() + .AddTransient, LoRaDeviceModelMapper>() + .AddTransient(); + } + + private static IServiceCollection ConfigureServices(this IServiceCollection services) + { + return services.AddTransient(); + } + + private static IServiceCollection ConfigureHealthCheck(this IServiceCollection services) + { + _ = services.AddHealthChecks() + .AddDbContextCheck() + .AddCheck("iothubHealth") + .AddCheck("storageAccountHealth") + .AddCheck("tableStorageHealth") + .AddCheck("dpsHealth") + .AddCheck("loraManagementFacadeHealth") + .AddCheck("databaseHealthCheck"); + + return services; + } + + private static IServiceCollection ConfigureImageBlobStorage(this IServiceCollection services, ConfigHandler configuration) + { + return services.AddTransient(_ => new BlobServiceClient(configuration.StorageAccountConnectionString)) + .Configure((opts) => + { + var serviceClient = new BlobServiceClient(configuration.StorageAccountConnectionString); + var container = serviceClient.GetBlobContainerClient(opts.ImageContainerName); + + _ = container.SetAccessPolicy(PublicAccessType.Blob); + _ = container.CreateIfNotExists(); + + opts.BaseUri = container.Uri; + }); + } + } +} diff --git a/src/AzureIoTHub.Portal.Server/AzureIoTHub.Portal.Server.csproj b/src/AzureIoTHub.Portal.Server/AzureIoTHub.Portal.Server.csproj index 5260ab508..5eba78410 100644 --- a/src/AzureIoTHub.Portal.Server/AzureIoTHub.Portal.Server.csproj +++ b/src/AzureIoTHub.Portal.Server/AzureIoTHub.Portal.Server.csproj @@ -31,7 +31,6 @@ - diff --git a/src/AzureIoTHub.Portal.Server/Startup.cs b/src/AzureIoTHub.Portal.Server/Startup.cs index 699d0853f..33c5a23a6 100644 --- a/src/AzureIoTHub.Portal.Server/Startup.cs +++ b/src/AzureIoTHub.Portal.Server/Startup.cs @@ -5,34 +5,20 @@ namespace AzureIoTHub.Portal.Server { using System; using System.IO; - using System.Net; using System.Threading.Tasks; - using Azure.Storage.Blobs; - using Azure.Storage.Blobs.Models; using AzureIoTHub.Portal.Application.Managers; - using AzureIoTHub.Portal.Application.Mappers; - using AzureIoTHub.Portal.Application.Providers; - using AzureIoTHub.Portal.Application.Services; - using AzureIoTHub.Portal.Application.Wrappers; - using AzureIoTHub.Portal.Domain.Options; - using AzureIoTHub.Portal.Infrastructure.Managers; - using AzureIoTHub.Portal.Infrastructure.Mappers; - using AzureIoTHub.Portal.Infrastructure.Providers; - using AzureIoTHub.Portal.Infrastructure.Services; + using AzureIoTHub.Portal.Application.Startup; using AzureIoTHub.Portal.Infrastructure.ServicesHealthCheck; - using AzureIoTHub.Portal.Infrastructure.Wrappers; + using AzureIoTHub.Portal.Infrastructure.Startup; using AzureIoTHub.Portal.Server.Jobs; using Domain; using Domain.Exceptions; - using Domain.Repositories; using EntityFramework.Exceptions.Common; - using EntityFramework.Exceptions.PostgreSQL; using Extensions; using Hellang.Middleware.ProblemDetails; using Hellang.Middleware.ProblemDetails.Mvc; using Identity; using Infrastructure; - using Infrastructure.Repositories; using Infrastructure.Seeds; using Managers; using Microsoft.AspNetCore.Authentication.JwtBearer; @@ -42,21 +28,15 @@ namespace AzureIoTHub.Portal.Server using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Versioning; - using Microsoft.Azure.Devices; - using Microsoft.Azure.Devices.Provisioning.Service; - using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; - using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.OpenApi.Models; using Models.v10; using Models.v10.LoRaWAN; using MudBlazor.Services; - using Polly; - using Polly.Extensions.Http; using Prometheus; using Quartz; using Services; @@ -85,71 +65,20 @@ public void ConfigureServices(IServiceCollection services) var configuration = ConfigHandlerFactory.Create(HostEnvironment, Configuration); - _ = services.Configure(opts => - { - opts.MetadataUrl = new Uri(configuration.OIDCMetadataUrl); - opts.ClientId = configuration.OIDCClientId; - opts.Scope = configuration.OIDCScope; - opts.Authority = configuration.OIDCAuthority; - }); - - _ = services.Configure(opts => - { - opts.Enabled = configuration.IsLoRaEnabled; - opts.KeyManagementApiVersion = configuration.LoRaKeyManagementApiVersion; - opts.KeyManagementCode = configuration.LoRaKeyManagementCode; - opts.KeyManagementUrl = configuration.LoRaKeyManagementUrl; - }); - - _ = services - .AddAuthentication(options => - { - options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; - }) - .AddJwtBearer(opts => - { - opts.Authority = configuration.OIDCAuthority; - opts.MetadataAddress = configuration.OIDCMetadataUrl; - opts.Audience = configuration.OIDCApiClientId; + _ = services.AddSingleton(configuration); - opts.TokenValidationParameters.ValidateIssuer = configuration.OIDCValidateIssuer; - opts.TokenValidationParameters.ValidateAudience = configuration.OIDCValidateAudience; - opts.TokenValidationParameters.ValidateLifetime = configuration.OIDCValidateLifetime; - opts.TokenValidationParameters.ValidateIssuerSigningKey = configuration.OIDCValidateIssuerSigningKey; - opts.TokenValidationParameters.ValidateActor = configuration.OIDCValidateActor; - opts.TokenValidationParameters.ValidateTokenReplay = configuration.OIDCValidateTokenReplay; - }); + _ = services.AddInfrastructureLayer(configuration) + .AddApplicationLayer(); - ConfigureDatabase(services, configuration); + AddAuthenticationAndAuthorization(services, configuration); - _ = services.AddSingleton(configuration); _ = services.AddSingleton(new PortalMetric()); _ = services.AddSingleton(new LoRaGatewayIDList()); _ = services.AddRazorPages(); - _ = services.AddScoped(_ => RegistryManager.CreateFromConnectionString(configuration.IoTHubConnectionString)); - - _ = services.AddScoped(_ => ServiceClient.CreateFromConnectionString(configuration.IoTHubConnectionString)); - - _ = services.AddScoped(_ => ProvisioningServiceClient.CreateFromConnectionString(configuration.DPSConnectionString)); - - _ = services.AddTransient(); - _ = services.AddTransient(_ => new BlobServiceClient(configuration.StorageAccountConnectionString)); - _ = services.AddTransient(); - _ = services.AddTransient(); - _ = services.AddTransient(); - _ = services.AddTransient(); - _ = services.AddTransient(); _ = services.AddTransient(); - _ = services.AddTransient, DeviceTwinMapper>(); - _ = services.AddTransient, LoRaDeviceTwinMapper>(); - _ = services.AddTransient, DeviceModelMapper>(); - _ = services.AddTransient, LoRaDeviceModelMapper>(); - _ = services.AddTransient(); - _ = services.AddTransient(); _ = services.AddTransient(); _ = services.AddTransient(); @@ -164,40 +93,8 @@ public void ConfigureServices(IServiceCollection services) _ = services.AddTransient, DeviceService>(); _ = services.AddTransient, LoRaWanDeviceService>(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddScoped(); - _ = services.AddMudServices(); - var transientHttpErrorPolicy = HttpPolicyExtensions - .HandleTransientHttpError() - .OrResult(c => c.StatusCode == HttpStatusCode.NotFound) - .WaitAndRetryAsync(3, _ => TimeSpan.FromMilliseconds(100)); - - _ = services.AddHttpClient("RestClient") - .AddPolicyHandler(transientHttpErrorPolicy); - - _ = services.AddHttpClient((sp, client) => - { - var opts = sp.GetService>().Value; - - client.BaseAddress = new Uri(opts.KeyManagementUrl); - client.DefaultRequestHeaders.Add("x-functions-key", opts.KeyManagementCode); - client.DefaultRequestHeaders.Add("api-version", opts.KeyManagementApiVersion ?? "2022-03-04"); - }) - .AddPolicyHandler(transientHttpErrorPolicy); - ConfigureIdeasFeature(services, configuration); // Add problem details support @@ -350,18 +247,6 @@ Specify the authorization token got from your IDP as a header. new HeaderApiVersionReader("X-Version")); }); - // Add AutoMapper Configuration - _ = services.AddAutoMapper(typeof(Startup), typeof(Application.Mappers.DeviceProfile)); - - _ = services.AddHealthChecks() - .AddDbContextCheck() - .AddCheck("iothubHealth") - .AddCheck("storageAccountHealth") - .AddCheck("tableStorageHealth") - .AddCheck("dpsHealth") - .AddCheck("loraManagementFacadeHealth") - .AddCheck("databaseHealthCheck"); - // Add the required Quartz.NET services _ = services.AddQuartz(q => { @@ -431,18 +316,6 @@ Specify the authorization token got from your IDP as a header. // Add the Quartz.NET hosted service _ = services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); - - // Options - _ = services.Configure((opts) => - { - var serviceClient = new BlobServiceClient(configuration.StorageAccountConnectionString); - var container = serviceClient.GetBlobContainerClient(opts.ImageContainerName); - - _ = container.SetAccessPolicy(PublicAccessType.Blob); - _ = container.CreateIfNotExists(); - - opts.BaseUri = container.Uri; - }); } private static void ConfigureIdeasFeature(IServiceCollection services, ConfigHandler configuration) @@ -458,25 +331,35 @@ private static void ConfigureIdeasFeature(IServiceCollection services, ConfigHan }); } - private static void ConfigureDatabase(IServiceCollection services, ConfigHandler configuration) + private static void AddAuthenticationAndAuthorization(IServiceCollection services, ConfigHandler configuration) { + _ = services.Configure(opts => + { + opts.MetadataUrl = new Uri(configuration.OIDCMetadataUrl); + opts.ClientId = configuration.OIDCClientId; + opts.Scope = configuration.OIDCScope; + opts.Authority = configuration.OIDCAuthority; + }); + _ = services - .AddDbContextPool(opts => + .AddAuthentication(options => { - _ = opts.UseNpgsql(configuration.PostgreSQLConnectionString); - _ = opts.UseExceptionProcessor(); - }); - - if (string.IsNullOrEmpty(configuration.PostgreSQLConnectionString)) - return; - - _ = services.AddScoped>(); - - var dbContextOptions = new DbContextOptionsBuilder(); - _ = dbContextOptions.UseNpgsql(configuration.PostgreSQLConnectionString); + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(opts => + { + opts.Authority = configuration.OIDCAuthority; + opts.MetadataAddress = configuration.OIDCMetadataUrl; + opts.Audience = configuration.OIDCApiClientId; - using var ctx = new PortalDbContext(dbContextOptions.Options); - ctx.Database.Migrate(); + opts.TokenValidationParameters.ValidateIssuer = configuration.OIDCValidateIssuer; + opts.TokenValidationParameters.ValidateAudience = configuration.OIDCValidateAudience; + opts.TokenValidationParameters.ValidateLifetime = configuration.OIDCValidateLifetime; + opts.TokenValidationParameters.ValidateIssuerSigningKey = configuration.OIDCValidateIssuerSigningKey; + opts.TokenValidationParameters.ValidateActor = configuration.OIDCValidateActor; + opts.TokenValidationParameters.ValidateTokenReplay = configuration.OIDCValidateTokenReplay; + }); } ///