diff --git a/src/Distech.CloudRelay.API/Distech.CloudRelay.API.csproj b/src/Distech.CloudRelay.API/Distech.CloudRelay.API.csproj
index ebe3b38..d142e23 100644
--- a/src/Distech.CloudRelay.API/Distech.CloudRelay.API.csproj
+++ b/src/Distech.CloudRelay.API/Distech.CloudRelay.API.csproj
@@ -1,7 +1,7 @@
- netcoreapp2.1
+ net6.0
true
Distech.CloudRelay.API
@@ -11,20 +11,13 @@
-
-
-
-
-
-
+
+
+
-
-
-
-
diff --git a/src/Distech.CloudRelay.API/Model/DeviceRequestHeaders.cs b/src/Distech.CloudRelay.API/Model/DeviceRequestHeaders.cs
index 82cae4f..2b6ac86 100644
--- a/src/Distech.CloudRelay.API/Model/DeviceRequestHeaders.cs
+++ b/src/Distech.CloudRelay.API/Model/DeviceRequestHeaders.cs
@@ -20,19 +20,19 @@ public class DeviceRequestHeaders
///
/// Gets or sets the content-type header.
///
- [JsonProperty(PropertyName = HeaderNames.ContentType, NullValueHandling = NullValueHandling.Ignore)]
+ [JsonProperty(PropertyName = "Content-Type", NullValueHandling = NullValueHandling.Ignore)]
public string ContentType { get; set; }
///
/// Gets or sets the content-disposition header.
///
- [JsonProperty(PropertyName = HeaderNames.ContentDisposition, NullValueHandling = NullValueHandling.Ignore)]
+ [JsonProperty(PropertyName = "Content-Disposition", NullValueHandling = NullValueHandling.Ignore)]
public string ContentDisposition { get; set; }
///
/// Gets or sets the content-length header.
///
- [JsonProperty(PropertyName = HeaderNames.ContentLength, NullValueHandling = NullValueHandling.Ignore)]
+ [JsonProperty(PropertyName = "Content-Length", NullValueHandling = NullValueHandling.Ignore)]
public long? ContentLength { get; set; }
#endregion
@@ -52,7 +52,10 @@ public DeviceRequestHeaders()
///
public DeviceRequestHeaders(HttpRequest request)
{
- Accept = request.GetTypedHeaders().Accept?.Select(a => a.ToString()).Aggregate((accept1, accept2) => $"{accept1}, {accept2}");
+ Accept = request.GetTypedHeaders().Accept
+ .Select(a => a.ToString())
+ .DefaultIfEmpty() //aggregate does not support empty sequence
+ .Aggregate((accept1, accept2) => $"{accept1}, {accept2}");
ContentType = request.GetTypedHeaders().ContentType?.ToString();
ContentDisposition = request.GetTypedHeaders().ContentDisposition?.ToString();
ContentLength = request.ContentLength;
diff --git a/src/Distech.CloudRelay.API/Model/DeviceResponseHeaders.cs b/src/Distech.CloudRelay.API/Model/DeviceResponseHeaders.cs
index 9c49811..1307e51 100644
--- a/src/Distech.CloudRelay.API/Model/DeviceResponseHeaders.cs
+++ b/src/Distech.CloudRelay.API/Model/DeviceResponseHeaders.cs
@@ -16,13 +16,13 @@ public class DeviceResponseHeaders
///
/// Gets or sets the content-type header.
///
- [JsonProperty(PropertyName = HeaderNames.ContentType)]
+ [JsonProperty(PropertyName = "Content-Type")]
public string ContentType { get; set; }
///
/// Gets or sets the content-disposition header.
///
- [JsonProperty(PropertyName = HeaderNames.ContentDisposition)]
+ [JsonProperty(PropertyName = "Content-Disposition")]
public string ContentDisposition { get; set; }
#endregion
diff --git a/src/Distech.CloudRelay.API/Program.cs b/src/Distech.CloudRelay.API/Program.cs
index 4ccdaae..811ce27 100644
--- a/src/Distech.CloudRelay.API/Program.cs
+++ b/src/Distech.CloudRelay.API/Program.cs
@@ -2,21 +2,148 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading.Tasks;
-using Microsoft.AspNetCore;
+using Distech.CloudRelay.API;
+using Distech.CloudRelay.API.Middleware;
+using Distech.CloudRelay.API.Model;
+using Distech.CloudRelay.API.Options;
+using Distech.CloudRelay.API.Services;
+using Microsoft.ApplicationInsights.Extensibility;
+using Microsoft.AspNetCore.Authentication;
+using Microsoft.AspNetCore.Authentication.AzureAD.UI;
+using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Authorization;
+using Microsoft.Azure.Devices;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Options;
+using Microsoft.Net.Http.Headers;
-namespace Distech.CloudRelay.API
+var builder = WebApplication.CreateBuilder(args);
+
+//inject authentication related entities and services
+#pragma warning disable 0618 //requires breaking changes to replace with Microsoft.Identity.Web
+builder.Services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
+#pragma warning restore 0618
+ .AddAzureADBearer(options => builder.Configuration.Bind(ApiEnvironment.AzureADOptions, options));
+
+if (builder.Environment.IsDevelopment())
{
- public class Program
+ //add diagnostics support for the JwtBearer middleware
+#pragma warning disable 0618 //requires breaking changes to replace with Microsoft.Identity.Web
+ builder.Services.PostConfigure(AzureADDefaults.JwtBearerAuthenticationScheme, static options =>
+#pragma warning restore 0618
{
- public static void Main(string[] args)
+ options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
+ });
+}
+
+//inject common services
+builder.Services.AddCloudRelayFileService(static (serviceProvider, options) =>
+{
+ var config = serviceProvider.GetRequiredService();
+ config.GetSection(ApiEnvironment.FileStorageSectionKey).Bind(options);
+
+ var adapterOptions = serviceProvider.GetService>();
+ options.ConnectionString = ApiEnvironment.GetConnectionString(ApiEnvironment.ResourceType.StorageAccount, adapterOptions.Value.EnvironmentId, config);
+
+ // files uploaded by the server are stored in a dedicated sub folder
+ options.ServerFileUploadSubFolder = adapterOptions.Value.MethodName;
+});
+
+//inject local services
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped(static serviceProvider =>
+{
+ var config = serviceProvider.GetRequiredService();
+ var options = serviceProvider.GetService>();
+ var connectionString = ApiEnvironment.GetConnectionString(ApiEnvironment.ResourceType.IoTHubService, options.Value.EnvironmentId, config);
+ return ServiceClient.CreateFromConnectionString(connectionString);
+});
+builder.Services.ConfigureOptions();
+
+//enables Application Insights telemetry (APPINSIGHTS_INSTRUMENTATIONKEY and ApplicationInsights:InstrumentationKey are supported)
+builder.Services.AddApplicationInsightsTelemetry(static options =>
+{
+ options.ApplicationVersion = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion;
+});
+builder.Services.AddApplicationInsightsTelemetryProcessor();
+builder.Services.AddSingleton();
+
+//enables CORS using the default policy
+builder.Services.AddCors(static options =>
+{
+ options.AddDefaultPolicy(static corsBuilder =>
+ {
+ corsBuilder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders(HeaderNames.ContentDisposition);
+ });
+});
+
+//allow accessing the HttpContext inside a service class
+builder.Services.AddHttpContextAccessor();
+
+builder.Services.AddControllers(configure =>
+{
+ // 'AzureAD' is currently the only supported authentication provider name and anything else will allow anonymous access
+ if (IdentityProviders.AzureActiveDirectory.Equals(builder.Configuration.GetSection(ApiEnvironment.AuthenticationProvider).Value, StringComparison.OrdinalIgnoreCase))
+ {
+ AuthorizationPolicy policy;
+
+ string[] roles = builder.Configuration.GetSection(ApiEnvironment.AzureADRoles).GetChildren().Select(role => role.Value).ToArray();
+ string[] scopes = builder.Configuration.GetSection(ApiEnvironment.AzureADScopes).GetChildren().Select(role => role.Value).ToArray();
+
+ if (roles.Length > 0)
+ {
+ // authentication + authorization based on application permissions aka roles
+ policy = new AuthorizationPolicyBuilder().RequireRole(roles).Build();
+ }
+ else if (scopes.Length > 0)
+ {
+ // authentication + authorization based on delegated permissions aka scopes
+ policy = new AuthorizationPolicyBuilder().RequireAssertion((ctx =>
+ {
+ var scopeClaim = ctx.User.FindFirst(c => c.Type == "http://schemas.microsoft.com/identity/claims/scope")?.Value.Split(' ');
+ return scopeClaim != null && scopes.All(s => scopeClaim.Contains(s));
+ }))
+ .Build();
+ }
+ else
{
- CreateWebHostBuilder(args).Build().Run();
+ // authentication only
+ policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
}
- public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
- WebHost.CreateDefaultBuilder(args)
- .UseStartup();
+ configure.Filters.Add(new AuthorizeFilter(policy));
}
+});
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseDeveloperExceptionPage();
}
+
+app.UseCors();
+app.UseHttpsRedirection();
+
+app.UseAuthentication();
+app.UseAuthorization();
+
+app.UseApplicationErrorHandler();
+app.UseMultiPartRequestBuffering();
+
+app.MapControllers();
+
+app.Run();
+
+///
+/// Intended for test projects using WebApplicationFactory.
+///
+public partial class Program { }
diff --git a/src/Distech.CloudRelay.API/Properties/launchSettings.json b/src/Distech.CloudRelay.API/Properties/launchSettings.json
index 95deec9..a35b209 100644
--- a/src/Distech.CloudRelay.API/Properties/launchSettings.json
+++ b/src/Distech.CloudRelay.API/Properties/launchSettings.json
@@ -9,19 +9,19 @@
}
},
"profiles": {
- "IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
+ "Distech.CloudRelay.API": {
+ "commandName": "Project",
+ "launchBrowser": false,
"launchUrl": "api",
+ "applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
- "Distech.CloudRelay.API": {
- "commandName": "Project",
- "launchBrowser": true,
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": false,
"launchUrl": "api",
- "applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/src/Distech.CloudRelay.API/Startup.cs b/src/Distech.CloudRelay.API/Startup.cs
deleted file mode 100644
index 71dfc5f..0000000
--- a/src/Distech.CloudRelay.API/Startup.cs
+++ /dev/null
@@ -1,157 +0,0 @@
-using Distech.CloudRelay.API.Middleware;
-using Distech.CloudRelay.API.Model;
-using Distech.CloudRelay.API.Options;
-using Distech.CloudRelay.API.Services;
-using Microsoft.ApplicationInsights.Extensibility;
-using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Authentication.AzureAD.UI;
-using Microsoft.AspNetCore.Authentication.JwtBearer;
-using Microsoft.AspNetCore.Authorization;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.AspNetCore.Mvc;
-using Microsoft.AspNetCore.Mvc.Authorization;
-using Microsoft.Azure.Devices;
-using Microsoft.Extensions.Configuration;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Options;
-using Microsoft.Net.Http.Headers;
-using System;
-using System.Linq;
-using System.Reflection;
-
-namespace Distech.CloudRelay.API
-{
- public class Startup
- {
- public IConfiguration Configuration { get; }
-
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
-
- public void ConfigureDevelopmentServices(IServiceCollection services)
- {
- //add diagnostics support for the JwtBearer middleware
- services.PostConfigure(AzureADDefaults.JwtBearerAuthenticationScheme, options =>
- {
- options.Events = JwtBearerMiddlewareDiagnostics.Subscribe(options.Events);
- });
-
- ConfigureServices(services);
- }
-
- public void ConfigureServices(IServiceCollection services)
- {
- //inject authentication related entities and services
- services.AddAuthentication(AzureADDefaults.BearerAuthenticationScheme)
- .AddAzureADBearer(options => Configuration.Bind(ApiEnvironment.AzureADOptions, options));
-
- //inject common services
- services.AddCloudRelayFileService((serviceProvider, options) =>
- {
- Configuration.GetSection(ApiEnvironment.FileStorageSectionKey).Bind(options);
-
- var adapterOptions = serviceProvider.GetService>();
- options.ConnectionString = ApiEnvironment.GetConnectionString(ApiEnvironment.ResourceType.StorageAccount, adapterOptions.Value.EnvironmentId, Configuration);
-
- // files uploaded by the server are stored in a dedicated sub folder
- options.ServerFileUploadSubFolder = adapterOptions.Value.MethodName;
- });
-
- //inject local services
- services.AddScoped();
- services.AddScoped();
- services.AddScoped(serviceProvider =>
- {
- var options = serviceProvider.GetService>();
- var connectionString = ApiEnvironment.GetConnectionString(ApiEnvironment.ResourceType.IoTHubService, options.Value.EnvironmentId, Configuration);
- return ServiceClient.CreateFromConnectionString(connectionString);
- });
- services.ConfigureOptions();
-
- //enables Application Insights telemetry (APPINSIGHTS_INSTRUMENTATIONKEY and ApplicationInsights:InstrumentationKey are supported)
- services.AddApplicationInsightsTelemetry(options =>
- {
- options.ApplicationVersion = Assembly.GetEntryAssembly().GetCustomAttribute().InformationalVersion;
- });
- services.AddApplicationInsightsTelemetryProcessor();
- services.AddSingleton();
-
- //enables CORS using the default policy
- services.AddCors((options =>
- {
- options.AddDefaultPolicy(builder =>
- {
- builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().WithExposedHeaders(HeaderNames.ContentDisposition);
- });
- }));
-
- services.AddMvc(opts =>
- {
- // 'AzureAD' is currently the only supported authentication provider name and anything else will allow anonymous access
- if (IdentityProviders.AzureActiveDirectory.Equals(Configuration.GetSection(ApiEnvironment.AuthenticationProvider).Value, StringComparison.OrdinalIgnoreCase))
- {
- AuthorizationPolicy policy;
-
- string[] roles = Configuration.GetSection(ApiEnvironment.AzureADRoles).GetChildren().Select(role => role.Value).ToArray();
- string[] scopes = Configuration.GetSection(ApiEnvironment.AzureADScopes).GetChildren().Select(role => role.Value).ToArray();
-
- if (roles.Count() > 0)
- {
- // authentication + authorization based on application permissions aka roles
- policy = new AuthorizationPolicyBuilder().RequireRole(roles).Build();
- }
- else if (scopes.Count() > 0)
- {
- // authentication + authorization based on delegated permissions aka scopes
- policy = new AuthorizationPolicyBuilder().RequireAssertion((ctx =>
- {
- var scopeClaim = ctx.User.FindFirst(c => c.Type == "http://schemas.microsoft.com/identity/claims/scope")?.Value.Split(' ');
- return scopeClaim != null && scopes.All(s => scopeClaim.Contains(s));
- }))
- .Build();
- }
- else
- {
- // authentication only
- policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
- }
-
- opts.Filters.Add(new AuthorizeFilter(policy));
- }
- })
- .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
-
- //allow accessing the HttpContext inside a service class
- services.AddHttpContextAccessor();
- }
-
- public void Configure(IApplicationBuilder app, IHostingEnvironment env)
- {
- if (env.IsDevelopment())
- {
- app.UseDeveloperExceptionPage();
- }
- else
- {
- app.UseHsts();
- }
-
- app.UseCors();
- app.UseHttpsRedirection();
- ConfigureAuthentication(app);
-
- app.UseApplicationErrorHandler();
- app.UseMultiPartRequestBuffering();
-
- app.UseMvc();
- }
-
- protected virtual void ConfigureAuthentication(IApplicationBuilder app)
- {
- app.UseAuthentication();
- }
- }
-}
diff --git a/src/Distech.CloudRelay.API/appsettings.Development.json b/src/Distech.CloudRelay.API/appsettings.Development.json
index e203e94..53d03ea 100644
--- a/src/Distech.CloudRelay.API/appsettings.Development.json
+++ b/src/Distech.CloudRelay.API/appsettings.Development.json
@@ -1,4 +1,22 @@
{
+ "Authentication": {
+ "AzureAD": {
+ "Instance": "https://login.microsoftonline.com/",
+ "TenantId": "00000000-0000-0000-0000-000000000000",
+ "ClientId": "00000000-0000-0000-0000-000000000000"
+ }
+ },
+ "DeviceCommunication": {
+ "IoTHub": {
+ "MethodName": "RestApi",
+ "MessageSizeThreshold": "16384",
+ "ResponseTimeout": "30"
+ }
+ },
+ "FileStorage": {
+ "DeviceFileUploadFolder": "iothub-fileupload",
+ "ServerFileUploadFolder": "cloud-relay-fileupload"
+ },
"Logging": {
"LogLevel": {
"Default": "Debug",
diff --git a/src/Distech.CloudRelay.API/appsettings.json b/src/Distech.CloudRelay.API/appsettings.json
index 5be7fcb..ce5c87c 100644
--- a/src/Distech.CloudRelay.API/appsettings.json
+++ b/src/Distech.CloudRelay.API/appsettings.json
@@ -1,5 +1,5 @@
{
- /* Required for the test execution to succeed when using the Production environment */
+ //TODO: remove on next breaking change
"Authentication": {
"AzureAD": {
"Instance": "https://login.microsoftonline.com/"
diff --git a/src/Distech.CloudRelay.Common/DAL/AzureBlob.cs b/src/Distech.CloudRelay.Common/DAL/AzureBlob.cs
index e3be172..c0515cb 100644
--- a/src/Distech.CloudRelay.Common/DAL/AzureBlob.cs
+++ b/src/Distech.CloudRelay.Common/DAL/AzureBlob.cs
@@ -1,4 +1,4 @@
-using Microsoft.WindowsAzure.Storage.Blob;
+using Azure.Storage.Blobs.Models;
using System;
using System.Collections.Generic;
using System.Text;
@@ -27,18 +27,38 @@ private AzureBlob()
///
/// Create a new from an Azure storage blob properties.
///
+ ///
///
///
- public static AzureBlob FromBlobProperties(string blobPath, BlobProperties blobProperties, IDictionary blobMetadata)
+ public static AzureBlob FromBlobProperties(string blobPath, BlobProperties blobProperties)
{
return new AzureBlob
{
Path = blobPath,
- Checksum = blobProperties.ContentMD5,
+ Checksum = Convert.ToBase64String(blobProperties.ContentHash),
ContentType = blobProperties.ContentType,
- LastModified = blobProperties.LastModified.Value,
- Length = blobProperties.Length,
- Metadata = BlobMetadata.FromDictionary(blobMetadata)
+ LastModified = blobProperties.LastModified,
+ Length = blobProperties.ContentLength,
+ Metadata = BlobMetadata.FromDictionary(blobProperties.Metadata)
+ };
+ }
+
+ ///
+ /// Create a new from a instance.
+ ///
+ ///
+ ///
+ ///
+ public static AzureBlob FromBlobItem(string blobPath, BlobItem item)
+ {
+ return new AzureBlob
+ {
+ Path = blobPath,
+ Checksum = Convert.ToBase64String(item.Properties.ContentHash),
+ ContentType = item.Properties.ContentType,
+ LastModified = item.Properties.LastModified ?? DateTimeOffset.MinValue,
+ Length = item.Properties.ContentLength ?? -1,
+ Metadata = BlobMetadata.FromDictionary(item.Metadata)
};
}
diff --git a/src/Distech.CloudRelay.Common/DAL/AzureBlobStorageBlobRepository.cs b/src/Distech.CloudRelay.Common/DAL/AzureBlobStorageBlobRepository.cs
index 2750e76..8ed6bf9 100644
--- a/src/Distech.CloudRelay.Common/DAL/AzureBlobStorageBlobRepository.cs
+++ b/src/Distech.CloudRelay.Common/DAL/AzureBlobStorageBlobRepository.cs
@@ -1,15 +1,16 @@
-using Distech.CloudRelay.Common.Exceptions;
+using Azure;
+using Azure.Storage.Blobs;
+using Azure.Storage.Blobs.Models;
+using Azure.Storage.Sas;
+using Distech.CloudRelay.Common.Exceptions;
using Distech.CloudRelay.Common.Options;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Microsoft.WindowsAzure.Storage;
-using Microsoft.WindowsAzure.Storage.Blob;
-using Microsoft.WindowsAzure.Storage.Blob.Protocol;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using System.Text;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Distech.CloudRelay.Common.DAL
@@ -22,7 +23,7 @@ internal class AzureStorageBlobRepository
{
#region Members
- private readonly Lazy m_LazyBlobClient;
+ private readonly Lazy m_LazyBlobServiceClient;
private readonly ILogger m_Logger;
@@ -37,10 +38,17 @@ internal class AzureStorageBlobRepository
///
public AzureStorageBlobRepository(IOptionsSnapshot options, ILogger logger)
{
- m_LazyBlobClient = new Lazy(() =>
+ //Note: Continues to rely on injecting options and creating client locally instead of relying on Microsoft.Azure.Extension
+ // to rely on DI instead of BlobServiceClient.
+ // Why: such clients are registered as Singleton by default, which is not compatible with current IOptions implementation
+ // reying on HttpContext to resolve proper storage account/connection strings based on environment associated to request/tenant.
+ // We end-up having a singleton instance (the client) having a depencency on IOptionsShapshot, which needs
+ // to remain scoped. Even if it worked, only the first connection string will be accounted for...
+ // In that case we need to discover the supported storage accounts ahead of time on API side to register any possible client
+ // instances as named instance and rely on some sort of locator/scoped instance instead on the service side.
+ m_LazyBlobServiceClient = new Lazy(() =>
{
- CloudStorageAccount account = CloudStorageAccount.Parse(options.Value.ConnectionString);
- return account.CreateCloudBlobClient();
+ return new BlobServiceClient(options.Value.ConnectionString);
});
m_Logger = logger;
}
@@ -57,14 +65,16 @@ public AzureStorageBlobRepository(IOptionsSnapshot o
public async Task GetBlobInfoAsync(string blobPath)
{
BlobInfo result = null;
+ BlobClient blob = GetBlobClient(blobPath);
- CloudBlobClient blobClient = m_LazyBlobClient.Value;
- CloudBlob blob = new CloudBlob(GetAbsoluteBlobUri(blobPath), blobClient.Credentials);
-
- //`ExistsAsync` also refreshes properties and metadata at the same times
- if (await blob.ExistsAsync())
+ try
{
- result = AzureBlob.FromBlobProperties(GetBlobPath(blob), blob.Properties, blob.Metadata);
+ var properties = await blob.GetPropertiesAsync();
+ result = AzureBlob.FromBlobProperties(GetBlobPath(blob), properties);
+ }
+ catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound || ex.ErrorCode == BlobErrorCode.ContainerNotFound)
+ {
+ //swallow does not exist error and return default null instead (as previous behavior before dotnet6 migration)
}
return result;
@@ -84,28 +94,21 @@ public async Task> ListBlobAsync(string blobPathPrefix)
throw new ArgumentException(nameof(blobPathPrefix));
}
- CloudBlobClient blobClient = m_LazyBlobClient.Value;
- BlobContinuationToken continuationToken = null;
+ BlobContainerClient containerClient = GetContainerClient(blobPathPrefix);
+ List result = new();
- //pad prefix path for container only with trailing slash '/', otherwise storage SDK list in $root container only
- if (!prefix.Contains('/'))
- {
- prefix += '/';
- }
-
- var result = new List();
+ //resolve any virtual folder prefix remaining without container info
+ prefix = Regex.Replace(blobPathPrefix, $"^[/]?{containerClient.Name}", string.Empty);
try
{
- do
+ var segment = containerClient.GetBlobsAsync(prefix: prefix, traits: BlobTraits.Metadata).AsPages();
+ await foreach (Page blobPage in segment)
{
- BlobResultSegment segment = await blobClient.ListBlobsSegmentedAsync(prefix, true, BlobListingDetails.Metadata, null, continuationToken, null, null);
- continuationToken = segment.ContinuationToken;
-
- result.AddRange(segment.Results.Cast().Select(b => AzureBlob.FromBlobProperties(GetBlobPath(b), b.Properties, b.Metadata)));
- } while (continuationToken != null);
+ result.AddRange(blobPage.Values.Select(b => AzureBlob.FromBlobItem(GetBlobPath(containerClient, b), b)));
+ }
}
- catch (StorageException ex) when (ex.RequestInformation.ErrorCode == BlobErrorCodeStrings.ContainerNotFound)
+ catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.ContainerNotFound)
{
//swallow container not found and let the default return the currently empty list instead
}
@@ -121,21 +124,21 @@ public async Task> ListBlobAsync(string blobPathPrefix)
///
public async Task OpenBlobAsync(string blobPath)
{
- CloudBlobClient blobClient = m_LazyBlobClient.Value;
- CloudBlob blob = new CloudBlob(GetAbsoluteBlobUri(blobPath), blobClient.Credentials);
+ BlobClient blob = GetBlobClient(blobPath);
try
{
+ BlobProperties properties = await blob.GetPropertiesAsync();
//blob properties and metadata are automatically fetched when opening the blob stream
Stream blobStream = await blob.OpenReadAsync();
- BlobInfo blobInfo = AzureBlob.FromBlobProperties(GetBlobPath(blob), blob.Properties, blob.Metadata);
+ BlobInfo blobInfo = AzureBlob.FromBlobProperties(GetBlobPath(blob), properties);
return new BlobStreamDecorator(blobStream, blobInfo);
}
- catch (StorageException ex) when (ex.RequestInformation.ErrorCode == BlobErrorCodeStrings.BlobNotFound || ex.RequestInformation.ErrorCode == BlobErrorCodeStrings.ContainerNotFound)
+ catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound || ex.ErrorCode == BlobErrorCode.ContainerNotFound)
{
- if (ex.RequestInformation.ErrorCode == BlobErrorCodeStrings.ContainerNotFound)
+ if (ex.ErrorCode == BlobErrorCode.ContainerNotFound)
{
- m_Logger.LogWarning($"Container '{blob.Container.Name}' does not exist");
+ m_Logger.LogWarning($"Container '{blob.BlobContainerName}' does not exist");
}
throw new IdNotFoundException(ErrorCodes.BlobNotFound, blobPath);
@@ -152,25 +155,23 @@ public async Task OpenBlobAsync(string blobPath)
///
public async Task WriteBlobAsync(string blobPath, BlobStreamDecorator data, bool overwrite = false)
{
- CloudBlobClient blobClient = m_LazyBlobClient.Value;
- CloudBlockBlob blockBlob = new CloudBlockBlob(GetAbsoluteBlobUri(blobPath), blobClient.Credentials);
+ BlobClient blob = GetBlobClient(blobPath);
try
{
//associate metadata to the blob, which will be saved by the next upload operation
- data.ApplyDecoratorTo(blockBlob);
+ BlobUploadOptions options = new();
+ data.ApplyDecoratorTo(options);
- //use IfNotExistsCondition by default to avoid overriding an existing blob without knowing about it
//access condition to ensure blob does not exist yet to catch concurrency issues
- AccessCondition condition = null;
if (!overwrite)
{
- condition = AccessCondition.GenerateIfNotExistsCondition();
+ options.Conditions = new BlobRequestConditions { IfNoneMatch = ETag.All };
}
- await blockBlob.UploadFromStreamAsync(data, condition, null, null);
+ await blob.UploadAsync(data, options);
}
- catch (StorageException ex) when (ex.RequestInformation.ErrorCode == BlobErrorCodeStrings.BlobAlreadyExists)
+ catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobAlreadyExists)
{
//blob already exists, overwriting is currently not implemented => concurrency error on the client side
throw new ConflictException(ErrorCodes.BlobAlreadyExists);
@@ -184,15 +185,14 @@ public async Task WriteBlobAsync(string blobPath, BlobStreamDecorator data, bool
/// Returns true if the blob has been deleted or false if the blob did not exist.
public async Task DeleteBlobAsync(string blobPath)
{
- CloudBlobClient blobClient = m_LazyBlobClient.Value;
- var blob = new CloudBlob(GetAbsoluteBlobUri(blobPath), blobClient.Credentials);
+ BlobClient blob = GetBlobClient(blobPath);
try
{
await blob.DeleteAsync();
return true;
}
- catch (StorageException ex) when (ex.RequestInformation.ErrorCode == BlobErrorCodeStrings.BlobNotFound)
+ catch (RequestFailedException ex) when (ex.ErrorCode == BlobErrorCode.BlobNotFound)
{
//swallow blob not found to ease delete operations
return false;
@@ -208,21 +208,53 @@ public async Task DeleteBlobAsync(string blobPath)
///
public async Task GetDelegatedReadAccessAsync(string blobPath, int secondsAccessExipiryDelay)
{
- CloudBlobClient blobClient = m_LazyBlobClient.Value;
- CloudBlob blob = new CloudBlob(GetAbsoluteBlobUri(blobPath), blobClient.Credentials);
+ BlobClient blob = GetBlobClient(blobPath);
if (!await blob.ExistsAsync())
{
throw new IdNotFoundException(ErrorCodes.BlobNotFound, blobPath);
}
- string sas = blob.GetSharedAccessSignature(new SharedAccessBlobPolicy()
+ Uri sasUri = blob.GenerateSasUri(BlobSasPermissions.Read, DateTimeOffset.UtcNow.AddSeconds(secondsAccessExipiryDelay));
+ return sasUri.ToString();
+ }
+
+ #endregion
+
+ #region Azure Clients
+
+ ///
+ /// Returns a client to manage the storage container specified in blob path.
+ ///
+ ///
+ ///
+ ///
+ private BlobContainerClient GetContainerClient(string blobRelativePath)
+ {
+ var segments = blobRelativePath.TrimStart('/').Split('/', 2);
+ if (segments.Length < 1)
{
- Permissions = SharedAccessBlobPermissions.Read,
- SharedAccessExpiryTime = DateTimeOffset.Now.AddSeconds(secondsAccessExipiryDelay)
- });
+ throw new ArgumentException($"Invalid blob path", nameof(blobRelativePath));
+ }
- return $"{blob.Uri}{sas}";
+ return m_LazyBlobServiceClient.Value.GetBlobContainerClient(segments[0]);
+ }
+
+ ///
+ /// Returns a client to manage the storage blob specified in blob path.
+ ///
+ ///
+ ///
+ ///
+ private BlobClient GetBlobClient(string blobRelativePath)
+ {
+ var segments = blobRelativePath.TrimStart('/').Split('/', 2);
+ if (segments.Length != 2)
+ {
+ throw new ArgumentException($"Invalid blob path", nameof(blobRelativePath));
+ }
+
+ return m_LazyBlobServiceClient.Value.GetBlobContainerClient(segments[0]).GetBlobClient(segments[1]);
}
#endregion
@@ -230,26 +262,24 @@ public async Task GetDelegatedReadAccessAsync(string blobPath, int secon
#region Helpers
///
- /// Returns the aboslute URI for the specified relative blob path.
+ /// Returns the relative path for the specified blob from the blob client base URI.
///
- ///
+ ///
///
- private Uri GetAbsoluteBlobUri(string blobRelativePath)
+ private static string GetBlobPath(BlobClient blob)
{
- //blob client abosulte uri contains '/' when using a real storage account, but does not for the emulator
- // - Azure storage account: "https://{accountName}.blob.core.windows.net/"
- // - Emulator: "http://127.0.0.1:10000/devstoreaccount1"
- return new Uri($"{m_LazyBlobClient.Value.BaseUri.AbsoluteUri.TrimEnd('/')}/{blobRelativePath.TrimStart('/')}");
+ return $"{blob.BlobContainerName}/{blob.Name}";
}
///
/// Returns the relative path for the specified blob from the blob client base URI.
///
- ///
+ ///
+ ///
///
- private string GetBlobPath(CloudBlob blob)
+ private static string GetBlobPath(BlobContainerClient container, BlobItem item)
{
- return $"{blob.Container.Name}/{blob.Name}";
+ return $"{container.Name}/{item.Name}";
}
#endregion
diff --git a/src/Distech.CloudRelay.Common/DAL/BlobMetadata.cs b/src/Distech.CloudRelay.Common/DAL/BlobMetadata.cs
index 4fe882f..ced14b9 100644
--- a/src/Distech.CloudRelay.Common/DAL/BlobMetadata.cs
+++ b/src/Distech.CloudRelay.Common/DAL/BlobMetadata.cs
@@ -1,4 +1,4 @@
-using Microsoft.WindowsAzure.Storage.Blob;
+using Azure.Storage.Blobs.Models;
using System;
using System.Collections;
using System.Collections.Generic;
@@ -71,7 +71,7 @@ internal static BlobMetadata FromDictionary(IDictionary metadata
/// Applies the metadata information to the specified azure storage blob.
///
///
- public void ApplyTo(CloudBlob blob)
+ public void ApplyTo(BlobUploadOptions blob)
{
foreach (var item in m_Metadata)
{
diff --git a/src/Distech.CloudRelay.Common/DAL/BlobStreamDecorator.cs b/src/Distech.CloudRelay.Common/DAL/BlobStreamDecorator.cs
index 325f2d7..5cb0c02 100644
--- a/src/Distech.CloudRelay.Common/DAL/BlobStreamDecorator.cs
+++ b/src/Distech.CloudRelay.Common/DAL/BlobStreamDecorator.cs
@@ -1,8 +1,10 @@
-using Microsoft.WindowsAzure.Storage.Blob;
+using Azure.Storage.Blobs.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
namespace Distech.CloudRelay.Common.DAL
{
@@ -103,16 +105,36 @@ protected override void Dispose(bool disposing)
public override long Position { get => m_Stream.Position; set => m_Stream.Position = value; }
+ public override Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken)
+ {
+ return m_Stream.CopyToAsync(destination, bufferSize, cancellationToken);
+ }
+
public override void Flush()
{
m_Stream.Flush();
}
+ public override Task FlushAsync(CancellationToken cancellationToken)
+ {
+ return m_Stream.FlushAsync(cancellationToken);
+ }
+
public override int Read(byte[] buffer, int offset, int count)
{
return m_Stream.Read(buffer, offset, count);
}
+ public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return m_Stream.ReadAsync(buffer, offset, count, cancellationToken);
+ }
+
+ public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default)
+ {
+ return m_Stream.ReadAsync(buffer, cancellationToken);
+ }
+
public override long Seek(long offset, SeekOrigin origin)
{
return m_Stream.Seek(offset, origin);
@@ -128,6 +150,16 @@ public override void Write(byte[] buffer, int offset, int count)
m_Stream.Write(buffer, offset, count);
}
+ public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
+ {
+ return m_Stream.WriteAsync(buffer, offset, count, cancellationToken);
+ }
+
+ public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
+ {
+ return m_Stream.WriteAsync(buffer, cancellationToken);
+ }
+
#endregion
#region Azure Blob Support
@@ -136,9 +168,10 @@ public override void Write(byte[] buffer, int offset, int count)
/// Applies the decorator information to the specified .
///
///
- public void ApplyDecoratorTo(CloudBlob blob)
+ public void ApplyDecoratorTo(BlobUploadOptions blob)
{
- blob.Properties.ContentType = ContentType;
+ blob.HttpHeaders ??= new BlobHttpHeaders();
+ blob.HttpHeaders.ContentType = ContentType;
Metadata?.ApplyTo(blob);
}
diff --git a/src/Distech.CloudRelay.Common/Distech.CloudRelay.Common.csproj b/src/Distech.CloudRelay.Common/Distech.CloudRelay.Common.csproj
index 10f4601..6f113c2 100644
--- a/src/Distech.CloudRelay.Common/Distech.CloudRelay.Common.csproj
+++ b/src/Distech.CloudRelay.Common/Distech.CloudRelay.Common.csproj
@@ -1,15 +1,15 @@
-
+
- netstandard2.0
+ net6.0
Distech.CloudRelay.Common
Common services and options for Distech Relay API assemblies
-
-
+
+
diff --git a/src/Distech.CloudRelay.Functions/Distech.CloudRelay.Functions.csproj b/src/Distech.CloudRelay.Functions/Distech.CloudRelay.Functions.csproj
index fd89193..bb693da 100644
--- a/src/Distech.CloudRelay.Functions/Distech.CloudRelay.Functions.csproj
+++ b/src/Distech.CloudRelay.Functions/Distech.CloudRelay.Functions.csproj
@@ -1,13 +1,13 @@
- netcoreapp2.1
- v2
+ net6.0
+ v4
92dc58da-6f07-4cf7-8571-ec5c9f11d03d
-
-
+
+
diff --git a/src/Distech.CloudRelay.Functions/local.settings.json b/src/Distech.CloudRelay.Functions/local.settings.json
index 04f6449..9a35c5c 100644
--- a/src/Distech.CloudRelay.Functions/local.settings.json
+++ b/src/Distech.CloudRelay.Functions/local.settings.json
@@ -6,6 +6,9 @@
"AzureFunctionsJobHost__Logging__LogLevel__Default": "Debug",
"AzureFunctionsJobHost__Logging__LogLevel__System": "Information",
- "AzureFunctionsJobHost__Logging__LogLevel__Microsoft": "Information"
+ "AzureFunctionsJobHost__Logging__LogLevel__Microsoft": "Information",
+ "FileStorage__DeviceFileUploadFolder": "iothub-fileupload",
+ "FileStorage__ServerFileUploadFolder": "cloud-relay-fileupload",
+ "FileStorage__ServerFileUploadSubFolder": "RestApi"
}
}
\ No newline at end of file
diff --git a/test/Distech.CloudRelay.API.FunctionalTests/CloudRelayAPIApplicationFactory.cs b/test/Distech.CloudRelay.API.FunctionalTests/CloudRelayAPIApplicationFactory.cs
index faf08a5..efd997a 100644
--- a/test/Distech.CloudRelay.API.FunctionalTests/CloudRelayAPIApplicationFactory.cs
+++ b/test/Distech.CloudRelay.API.FunctionalTests/CloudRelayAPIApplicationFactory.cs
@@ -1,7 +1,11 @@
-using Microsoft.AspNetCore;
+using Distech.CloudRelay.API.FunctionalTests.Middleware;
+using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.AspNetCore.TestHost;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using System;
using System.IO;
namespace Distech.CloudRelay.API.FunctionalTests
@@ -23,14 +27,39 @@ protected override void Dispose(bool disposing)
base.Dispose(disposing);
}
- protected override IWebHostBuilder CreateWebHostBuilder()
+ protected override void ConfigureWebHost(IWebHostBuilder builder)
{
- return WebHost.CreateDefaultBuilder(null);
+ builder.UseSolutionRelativeContentRoot(Path.Combine("src", "Distech.CloudRelay.API"));
+ builder.ConfigureServices(services =>
+ {
+ services.AddTransient();
+ });
}
- protected override void ConfigureWebHost(IWebHostBuilder builder)
+ class IdentityMiddlewareFilter
+ : IStartupFilter
{
- builder.UseSolutionRelativeContentRoot(Path.Combine("src", "Distech.CloudRelay.API"));
+ private readonly IConfiguration m_Configuration;
+
+ public IdentityMiddlewareFilter(IConfiguration configuration)
+ {
+ m_Configuration = configuration;
+ }
+
+ public Action Configure(Action next)
+ {
+ var configuration = m_Configuration;
+ return builder =>
+ {
+ if (configuration.GetValue(TestEnvironment.UseIdentity, false))
+ {
+ builder.UseMiddleware();
+ }
+
+ next(builder);
+ };
+
+ }
}
}
}
diff --git a/test/Distech.CloudRelay.API.FunctionalTests/DevicesTests.cs b/test/Distech.CloudRelay.API.FunctionalTests/DevicesTests.cs
index 63b25fd..25d01e8 100644
--- a/test/Distech.CloudRelay.API.FunctionalTests/DevicesTests.cs
+++ b/test/Distech.CloudRelay.API.FunctionalTests/DevicesTests.cs
@@ -9,23 +9,23 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net;
-using System.Net.Http;
+using System.Net.Http.Json;
using System.Threading.Tasks;
using Xunit;
namespace Distech.CloudRelay.API.FunctionalTests
{
- public class DevicesControllerTests : IClassFixture>
+ public class DevicesControllerTests : IClassFixture>
{
#region Member Variables
- private readonly CloudRelayAPIApplicationFactory m_ApiAppFactory;
+ private readonly CloudRelayAPIApplicationFactory m_ApiAppFactory;
#endregion
#region Constructors
- public DevicesControllerTests(CloudRelayAPIApplicationFactory apiAppFactory)
+ public DevicesControllerTests(CloudRelayAPIApplicationFactory apiAppFactory)
{
m_ApiAppFactory = apiAppFactory;
}
@@ -80,8 +80,7 @@ public async Task GetDeviceRequest_DeviceNotFound_ReturnsNotFound()
//Act
var response = await client.GetAsync($"api/v1/devices/{deviceId}/request");
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -108,8 +107,7 @@ public async Task GetDeviceRequest_EnvironmentNotFound_ReturnsNotFound()
//Act
var response = await client.GetAsync($"api/v1/devices/{deviceId}/request?environmentId=someId");
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -130,6 +128,8 @@ public async Task PostDeviceRequest_HappyPath_ReturnsOk()
builder.ConfigureTestServices(services =>
{
var stubAdapter = new Mock();
+ stubAdapter.Setup(a => a.GetMaximumMessageSize())
+ .Returns(int.MaxValue);
stubAdapter.Setup(a => a.InvokeCommandAsync(deviceId, It.IsAny()))
.ReturnsAsync(new InvocationResult(StatusCodes.Status200OK, GetMockedResponse(StatusCodes.Status200OK, new object[] { })));
services.AddScoped(_ => stubAdapter.Object);
@@ -155,6 +155,8 @@ public async Task PostDeviceRequest_DeviceNotFound_ReturnsNotFound()
builder.ConfigureTestServices(services =>
{
var stubAdapter = new Mock();
+ stubAdapter.Setup(a => a.GetMaximumMessageSize())
+ .Returns(int.MaxValue);
stubAdapter.Setup(a => a.InvokeCommandAsync(deviceId, It.IsAny()))
.ThrowsAsync(new IdNotFoundException(ErrorCodes.DeviceNotFound, deviceId));
services.AddScoped(_ => stubAdapter.Object);
@@ -165,8 +167,7 @@ public async Task PostDeviceRequest_DeviceNotFound_ReturnsNotFound()
//Act
var response = await client.PostAsJsonAsync($"api/v1/devices/{deviceId}/request", default(string));
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -193,8 +194,7 @@ public async Task PostDeviceRequest_EnvironmentNotFound_ReturnsNotFound()
//Act
var response = await client.PostAsJsonAsync($"api/v1/devices/{deviceId}/request?environmentId=someId", default(string));
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -215,6 +215,8 @@ public async Task PuDeviceRequest_HappyPath_ReturnsOk()
builder.ConfigureTestServices(services =>
{
var stubAdapter = new Mock();
+ stubAdapter.Setup(a => a.GetMaximumMessageSize())
+ .Returns(int.MaxValue);
stubAdapter.Setup(a => a.InvokeCommandAsync(deviceId, It.IsAny()))
.ReturnsAsync(new InvocationResult(StatusCodes.Status200OK, GetMockedLegacyResponse(StatusCodes.Status200OK, default(string))));
services.AddScoped(_ => stubAdapter.Object);
@@ -240,6 +242,8 @@ public async Task PutDeviceRequest_DeviceNotFound_ReturnsNotFound()
builder.ConfigureTestServices(services =>
{
var stubAdapter = new Mock();
+ stubAdapter.Setup(a => a.GetMaximumMessageSize())
+ .Returns(int.MaxValue);
stubAdapter.Setup(a => a.InvokeCommandAsync(deviceId, It.IsAny()))
.ThrowsAsync(new IdNotFoundException(ErrorCodes.DeviceNotFound, deviceId));
services.AddScoped(_ => stubAdapter.Object);
@@ -250,8 +254,7 @@ public async Task PutDeviceRequest_DeviceNotFound_ReturnsNotFound()
//Act
var response = await client.PutAsJsonAsync($"api/v1/devices/{deviceId}/request", default(string));
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -278,8 +281,7 @@ public async Task PutDeviceRequest_EnvironmentNotFound_ReturnsNotFound()
//Act
var response = await client.PutAsJsonAsync($"api/v1/devices/{deviceId}/request?environmentId=someId", default(string));
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -335,8 +337,7 @@ public async Task DeleteDeviceRequest_DeviceNotFound_ReturnsNotFound()
//Act
var response = await client.DeleteAsync($"api/v1/devices/{deviceId}/request");
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
@@ -363,8 +364,7 @@ public async Task DeleteDeviceRequest_EnvironmentNotFound_ReturnsNotFound()
//Act
var response = await client.DeleteAsync($"api/v1/devices/{deviceId}/request?environmentId=someId");
- var content = await response.Content.ReadAsStringAsync();
- ApplicationProblemDetails problem = JsonConvert.DeserializeObject(content);
+ var problem = await response.Content.ReadFromJsonAsync();
//Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
diff --git a/test/Distech.CloudRelay.API.FunctionalTests/Distech.CloudRelay.API.FunctionalTests.csproj b/test/Distech.CloudRelay.API.FunctionalTests/Distech.CloudRelay.API.FunctionalTests.csproj
index a57eabd..db45d94 100644
--- a/test/Distech.CloudRelay.API.FunctionalTests/Distech.CloudRelay.API.FunctionalTests.csproj
+++ b/test/Distech.CloudRelay.API.FunctionalTests/Distech.CloudRelay.API.FunctionalTests.csproj
@@ -1,18 +1,17 @@
- netcoreapp2.1
+ net6.0
false
-
-
-
-
-
-
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/test/Distech.CloudRelay.API.FunctionalTests/TestStartup.cs b/test/Distech.CloudRelay.API.FunctionalTests/TestStartup.cs
deleted file mode 100644
index e30304d..0000000
--- a/test/Distech.CloudRelay.API.FunctionalTests/TestStartup.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Distech.CloudRelay.API.FunctionalTests.Middleware;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.Extensions.Configuration;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-
-namespace Distech.CloudRelay.API.FunctionalTests
-{
- public class TestStartup : Startup
- {
- public TestStartup(IConfiguration configuration)
- : base(configuration)
- {
- }
-
- protected override void ConfigureAuthentication(IApplicationBuilder app)
- {
- base.ConfigureAuthentication(app);
-
- if (Configuration.GetValue(TestEnvironment.UseIdentity, false))
- app.UseMiddleware();
- }
- }
-}
diff --git a/test/Distech.CloudRelay.Common.UnitTests/Distech.CloudRelay.Common.UnitTests.csproj b/test/Distech.CloudRelay.Common.UnitTests/Distech.CloudRelay.Common.UnitTests.csproj
index 88b504e..54b2b71 100644
--- a/test/Distech.CloudRelay.Common.UnitTests/Distech.CloudRelay.Common.UnitTests.csproj
+++ b/test/Distech.CloudRelay.Common.UnitTests/Distech.CloudRelay.Common.UnitTests.csproj
@@ -1,20 +1,23 @@
-
+
- netcoreapp2.1
+ net6.0
false
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-