diff --git a/Directory.Build.props b/Directory.Build.props index 8978ebf..bcb8e35 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,6 +4,11 @@ d.velop AG Copyright (c) 2024 https://github.com/d-velop/dvelop-sdk-cs - 0.0.10 + 0.1.0 + README.md + + + + diff --git a/dvelop-sdk-all/DvelopSdk.csproj b/dvelop-sdk-all/DvelopSdk.csproj index eb6a721..80870a4 100644 --- a/dvelop-sdk-all/DvelopSdk.csproj +++ b/dvelop-sdk-all/DvelopSdk.csproj @@ -3,7 +3,7 @@ Dvelop.Sdk - netstandard2.0 + net8.0 Dvelop.Sdk Dvelop.Sdk Copyright (c) 2019 @@ -12,7 +12,7 @@ true false false - + https://github.com/d-velop/dvelop-sdk-cs https://github.com/d-velop/dvelop-sdk-cs @@ -21,10 +21,11 @@ true true snupkg + latestmajor - + diff --git a/dvelop-sdk-base/BaseDtos/BaseDtos.csproj b/dvelop-sdk-base/BaseDtos/BaseDtos.csproj index a30bb9f..69e4751 100644 --- a/dvelop-sdk-base/BaseDtos/BaseDtos.csproj +++ b/dvelop-sdk-base/BaseDtos/BaseDtos.csproj @@ -1,16 +1,17 @@  - netstandard2.0 + net8.0 Dvelop.Sdk.Base.Dto Dvelop.Sdk.Base.Dto true true snupkg + latestmajor - + diff --git a/dvelop-sdk-base/BaseInterfaces/BaseInterfaces.csproj b/dvelop-sdk-base/BaseInterfaces/BaseInterfaces.csproj index 2d8aa2f..8ddeb74 100644 --- a/dvelop-sdk-base/BaseInterfaces/BaseInterfaces.csproj +++ b/dvelop-sdk-base/BaseInterfaces/BaseInterfaces.csproj @@ -1,9 +1,10 @@ - netstandard2.0 + net8.0 Dvelop.Sdk.BaseInterfaces Dvelop.Sdk.BaseInterfaces + latestmajor diff --git a/dvelop-sdk-cloudcenter/CloudCenterDtos/CloudCenterDtos.csproj b/dvelop-sdk-cloudcenter/CloudCenterDtos/CloudCenterDtos.csproj index 3cb7693..16839af 100644 --- a/dvelop-sdk-cloudcenter/CloudCenterDtos/CloudCenterDtos.csproj +++ b/dvelop-sdk-cloudcenter/CloudCenterDtos/CloudCenterDtos.csproj @@ -8,10 +8,11 @@ true true snupkg + latestmajor - + diff --git a/dvelop-sdk-config/ConfigDtos/ConfigDtos.csproj b/dvelop-sdk-config/ConfigDtos/ConfigDtos.csproj index 71ae132..9a971db 100644 --- a/dvelop-sdk-config/ConfigDtos/ConfigDtos.csproj +++ b/dvelop-sdk-config/ConfigDtos/ConfigDtos.csproj @@ -1,17 +1,18 @@  - netstandard2.0 + net8.0 Dvelop.Sdk.Config.Dto Dvelop.Sdk.Config.Dto true true snupkg + latestmajor - - + + diff --git a/dvelop-sdk-dash/DashboardDtos/DashboardDtos.csproj b/dvelop-sdk-dash/DashboardDtos/DashboardDtos.csproj index 3ce032a..cd407ec 100644 --- a/dvelop-sdk-dash/DashboardDtos/DashboardDtos.csproj +++ b/dvelop-sdk-dash/DashboardDtos/DashboardDtos.csproj @@ -1,17 +1,18 @@  - netstandard2.0 + net8.0 Dvelop.Sdk.Dashboard.Dto Dvelop.Sdk.Dashboard.Dto true true snupkg + - - + + diff --git a/dvelop-sdk-home/HomeDtos/HomeDtos.csproj b/dvelop-sdk-home/HomeDtos/HomeDtos.csproj index 1964a97..3fa2276 100644 --- a/dvelop-sdk-home/HomeDtos/HomeDtos.csproj +++ b/dvelop-sdk-home/HomeDtos/HomeDtos.csproj @@ -1,17 +1,18 @@  - - netstandard2.0 - Dvelop.Sdk.Home.Dto - Dvelop.Sdk.Home.Dto - true - true - snupkg - + + net8.0 + Dvelop.Sdk.Home.Dto + Dvelop.Sdk.Home.Dto + true + true + snupkg + latestmajor + - - - + + + diff --git a/dvelop-sdk-httpclientextensions/HttpClientExtensions.UnitTest/HttpClientExtensions.UnitTest.csproj b/dvelop-sdk-httpclientextensions/HttpClientExtensions.UnitTest/HttpClientExtensions.UnitTest.csproj index 2f405dd..da8b72d 100644 --- a/dvelop-sdk-httpclientextensions/HttpClientExtensions.UnitTest/HttpClientExtensions.UnitTest.csproj +++ b/dvelop-sdk-httpclientextensions/HttpClientExtensions.UnitTest/HttpClientExtensions.UnitTest.csproj @@ -9,10 +9,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/dvelop-sdk-httpclientextensions/HttpClientExtensions/HttpClientExtensions.csproj b/dvelop-sdk-httpclientextensions/HttpClientExtensions/HttpClientExtensions.csproj index e9fc8e3..9ee9ce7 100644 --- a/dvelop-sdk-httpclientextensions/HttpClientExtensions/HttpClientExtensions.csproj +++ b/dvelop-sdk-httpclientextensions/HttpClientExtensions/HttpClientExtensions.csproj @@ -1,25 +1,26 @@ - netstandard2.0 + net8.0 true Dvelop.Sdk.HttpClientExtensions Dvelop.Sdk.HttpClientExtensions Library - - true - true - snupkg - + latestmajor - - - + true + true + snupkg + - - - + + + + + + + diff --git a/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderClient.csproj b/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderClient.csproj index 95bbd99..dab77d0 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderClient.csproj +++ b/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderClient.csproj @@ -1,23 +1,24 @@  - netstandard2.0 + net8.0 Dvelop.Sdk.IdentityProvider.Client Dvelop.Sdk.IdentityProvider.Client true true true snupkg + latestmajor - + - + diff --git a/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderSessionStore.cs b/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderSessionStore.cs index 54fbfac..4a90a42 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderSessionStore.cs +++ b/dvelop-sdk-identityprovider/IdentityProviderClient/IdentityProviderSessionStore.cs @@ -8,7 +8,7 @@ namespace Dvelop.Sdk.IdentityProvider.Client { public class IdentityProviderSessionStore { - private readonly ISystemClock _clock; + private readonly TimeProvider _clock; private readonly int _cleanupThreshold; private readonly ConcurrentDictionary _sessionCache = @@ -17,13 +17,13 @@ public class IdentityProviderSessionStore private int _cleanupCounter; private readonly object _cleanupLock=new object(); - public IdentityProviderSessionStore(ISystemClock clock, int cleanupThreshold) + public IdentityProviderSessionStore(TimeProvider clock, int cleanupThreshold) { _clock = clock; _cleanupThreshold = cleanupThreshold; } - public IdentityProviderSessionStore():this(new SystemClock(), 20) + public IdentityProviderSessionStore():this(TimeProvider.System, 20) { } @@ -34,7 +34,7 @@ public ClaimsPrincipal GetPrincipal(string cookie) //CleanUp(); var id = IdFromCookie(cookie); if (!_sessionCache.TryGetValue(id, out var sessionItem)) return null; - if (sessionItem.Expire.CompareTo(_clock.UtcNow) < 0) + if (sessionItem.Expire.CompareTo(_clock.GetUtcNow()) < 0) { _sessionCache.TryRemove(id,out _); return null; @@ -68,7 +68,7 @@ private void CleanUp() foreach (var key in _sessionCache.Keys) { if (!_sessionCache.TryGetValue(key, out var sessionItem)) continue; - if (sessionItem.Expire.CompareTo(_clock.UtcNow) < 0) + if (sessionItem.Expire.CompareTo(_clock.GetUtcNow()) < 0) { _sessionCache.TryRemove(key, out _); } diff --git a/dvelop-sdk-identityprovider/IdentityProviderDtos/IdentityProviderDtos.csproj b/dvelop-sdk-identityprovider/IdentityProviderDtos/IdentityProviderDtos.csproj index 6218754..ee4dcd1 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderDtos/IdentityProviderDtos.csproj +++ b/dvelop-sdk-identityprovider/IdentityProviderDtos/IdentityProviderDtos.csproj @@ -10,7 +10,7 @@ - + diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddleware.UnitTest.csproj b/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddleware.UnitTest.csproj index c4a2959..36a1b41 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddleware.UnitTest.csproj +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddleware.UnitTest.csproj @@ -6,14 +6,15 @@ Dvelop.Sdk.IdentityProviderMiddleware.UnitTest Dvelop.Sdk.IdentityProviderMiddleware.UnitTest Library + latestmajor - - - - - + + + + + diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddlewareTest.cs b/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddlewareTest.cs index cfa9821..3a3160f 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddlewareTest.cs +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderMiddlewareTest.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; @@ -10,8 +11,11 @@ using Dvelop.Sdk.IdentityProvider.Client; using Dvelop.Sdk.IdentityProvider.Middleware; using FluentAssertions; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Patterns; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -42,34 +46,64 @@ public void Setup() private static IEnumerable GetTestNoAuthSessionIdData() { - yield return new object[] { "IDP-1740_path", + yield return + [ + "IDP-1740_path", "GET", new Dictionary{{"Accept","text/html"}}, "/hüme", 302, new Dictionary { {"Location","/identityprovider/login?redirect=%2fh%25C3%25BCme"} - }, false}; + }, false + ]; - yield return new object[] { "IDP-1740_query", + yield return + [ + "IDP-1740_query", "GET", new Dictionary{{"Accept","text/html"}}, "/bla?path=%2Fh%C3%BCme", 302, new Dictionary { {"Location","/identityprovider/login?redirect=%2Fbla%3Fpath%3D%252Fh%25C3%25BCme"} - }, false}; + }, false + ]; - yield return new object[] { "GetRequestAndHtmlAccepted_Should_RedirectToIdp", - "GET", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false}; - yield return new object[] { "AndHeadRequestAndHtmlAccepted_Should_RedirectToIdp", - "HEAD", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1" }}, false}; - yield return new object[] { "BasicAuthorizationAndGetRequestAndHtmlAccepted_Should_RedirectsToIdp", - "GET", new Dictionary{{"Accept","text/html"},{"Authorization", "Basic adabdk"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false}; - yield return new object[] { "OtherCookieAndGetRequestAndHtmlAccepted_Should_RedirectsToIdp", - "GET", new Dictionary{{"Cookie","AnyCookie=adabdk"},{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false}; - yield return new object[] { "PostRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "POST", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false}; - yield return new object[] { "PutRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "PUT", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false}; - yield return new object[] { "DeleteRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "DELETE", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false}; - yield return new object[] { "PatchRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "PATCH", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false}; + yield return + [ + "GetRequestAndHtmlAccepted_Should_RedirectToIdp", + "GET", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false + ]; + yield return + [ + "AndHeadRequestAndHtmlAccepted_Should_RedirectToIdp", + "HEAD", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1" }}, false + ]; + yield return + [ + "BasicAuthorizationAndGetRequestAndHtmlAccepted_Should_RedirectsToIdp", + "GET", new Dictionary{{"Accept","text/html"},{"Authorization", "Basic adabdk"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false + ]; + yield return + [ + "OtherCookieAndGetRequestAndHtmlAccepted_Should_RedirectsToIdp", + "GET", new Dictionary{{"Cookie","AnyCookie=adabdk"},{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false + ]; + yield return + [ + "PostRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "POST", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false + ]; + yield return + [ + "PutRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "PUT", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false + ]; + yield return + [ + "DeleteRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "DELETE", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false + ]; + yield return + [ + "PatchRequestAndHtmlAccepted_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "PATCH", new Dictionary{{"Accept","text/html"}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"WWW-Authenticate","Bearer" }}, false + ]; } @@ -78,29 +112,53 @@ private static IEnumerable GetTestNoAuthSessionIdData() public async Task TestNoAuthSessionId(string testName, string requestMethod, Dictionary requestHeader, string requestUri, int expectedStatus, Dictionary expectedResponseHeader,bool allowExternalValidation) { Console.WriteLine(testName); - await TestMiddleWare(requestMethod,requestHeader,requestUri,expectedStatus,expectedResponseHeader,allowExternalValidation).ConfigureAwait(false); + await TestMiddleWare(requestMethod,requestHeader,requestUri,false,expectedStatus,expectedResponseHeader,allowExternalValidation).ConfigureAwait(false); } private static IEnumerable GetTestInvalidAuthSessionIdData() { const string invalidToken = "200e7388-1834-434b-be79-3745181e1457"; - yield return new object[] { "GetRequestAndHtmlAccepted_Middleware_RedirectsToIdp", - "GET", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false}; - yield return new object[] { "HeadRequestAndHtmlAccepted_Middleware_RedirectsToIdp", - "HEAD", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 302,new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false}; - yield return new object[] { "GetRequestAndHtmlNotAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "GET", new Dictionary{{"Accept", "application/json"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"Www-Authenticate","Bearer" }}, false}; - yield return new object[] { "PostRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "POST", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"Www-Authenticate","Bearer" }}, false}; - yield return new object[] { "PutRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "PUT", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"Www-Authenticate","Bearer" }}, false}; - yield return new object[] { "DeleteRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "DELETE", new Dictionary{{"Accept","text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401,new Dictionary{{"Www-Authenticate","Bearer" }}, false}; - yield return new object[] { "PatchRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", - "PATCH", new Dictionary{{"Accept","text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1",401, new Dictionary{{"Www-Authenticate","Bearer" }}, false}; - yield return new object[] { "GetRequestAndHtmlAcceptedAndExternalValidation_Middleware_RedirectsToIdp", - "GET", new Dictionary{{"Accept","text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, true}; + yield return + [ + "GetRequestAndHtmlAccepted_Middleware_RedirectsToIdp", + "GET", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false + ]; + yield return + [ + "HeadRequestAndHtmlAccepted_Middleware_RedirectsToIdp", + "HEAD", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 302,new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, false + ]; + yield return + [ + "GetRequestAndHtmlNotAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "GET", new Dictionary{{"Accept", "application/json"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"Www-Authenticate","Bearer" }}, false + ]; + yield return + [ + "PostRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "POST", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"Www-Authenticate","Bearer" }}, false + ]; + yield return + [ + "PutRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "PUT", new Dictionary{{"Accept", "text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401, new Dictionary{{"Www-Authenticate","Bearer" }}, false + ]; + yield return + [ + "DeleteRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "DELETE", new Dictionary{{"Accept","text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 401,new Dictionary{{"Www-Authenticate","Bearer" }}, false + ]; + yield return + [ + "PatchRequestAndHtmlAccepted_Middleware_ReturnsStatus401AndWWW-AuthenticateBearerHeader", + "PATCH", new Dictionary{{"Accept","text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1",401, new Dictionary{{"Www-Authenticate","Bearer" }}, false + ]; + yield return + [ + "GetRequestAndHtmlAcceptedAndExternalValidation_Middleware_RedirectsToIdp", + "GET", new Dictionary{{"Accept","text/html"}, {"Authorization", "Bearer " + invalidToken}}, "/a/b?q1=x&q2=1", 302, new Dictionary{{"Location","/identityprovider/login?redirect=%2Fa%2Fb%3Fq1%3Dx%26q2%3D1"}}, true + ]; } [DynamicData(nameof(GetTestInvalidAuthSessionIdData), DynamicDataSourceType.Method, DynamicDataDisplayName = "DisplayName")] @@ -110,42 +168,69 @@ public async Task TestInvalidAuthSessionId(string testName, string requestMethod Dictionary expectedResponseHeader, bool allowExternalValidation) { Console.WriteLine(testName); - await TestMiddleWare(requestMethod,requestHeader,requestUri,expectedStatus,expectedResponseHeader,allowExternalValidation).ConfigureAwait(false); + await TestMiddleWare(requestMethod,requestHeader,requestUri,false,expectedStatus,expectedResponseHeader,allowExternalValidation).ConfigureAwait(false); } private static IEnumerable GetTestNoAuthSessionIdAndGetRequestAndAcceptHeaderIsData() { - yield return new object[]{"", true}; - yield return new object[]{"text/", false}; - yield return new object[]{"text/*", true}; - yield return new object[]{"*/*", true}; - yield return new object[]{"application/json; q=1.0, */*; q=0.8", false}; // GO middleware says true - yield return new object[]{"text/html", true}; - yield return new object[]{"something/else", false}; - yield return new object[]{"text/html; q=1", true}; - yield return new object[]{"text/html; q=1.0", true}; - yield return new object[]{"text/html; q=0.9", true}; - yield return new object[]{"text/html; q=0", true}; // GO middleware says false - yield return new object[]{"text/html; q=0.0", true}; // GO middleware says false - yield return new object[]{"application/json", false}; - yield return new object[]{"application/json; q=1.0, text/html; q=0.9", false}; // GO middleware says true - yield return new object[]{"application/json; q=1.0, text/html; q=0", false}; - yield return new object[]{"application/json; q=0.9, text/html; q=1.0", true}; - yield return new object[]{"application/json; q=1.0, text/html; q=0.", false}; - yield return new object[]{"text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", true}; + yield return ["", true, true]; + yield return ["text/", true, false]; + yield return ["text/*",true, true]; + yield return ["*/*", true, true]; + yield return ["application/json; q=1.0, */*; q=0.8",true, false]; // GO middleware says true + yield return ["text/html", true, true]; + yield return ["something/else", true, false]; + yield return ["text/html; q=1", true, true]; + yield return ["text/html; q=1.0",true, true]; + yield return ["text/html; q=0.9",true, true]; + yield return ["text/html; q=0",true, true]; // GO middleware says false + yield return ["text/html; q=0.0",true, true]; // GO middleware says false + yield return ["application/json", true, false]; + yield return ["application/json; q=1.0, text/html; q=0.9",true, false]; // GO middleware says true + yield return ["application/json; q=1.0, text/html; q=0", true, false]; + yield return ["application/json; q=0.9, text/html; q=1.0", true, true]; + yield return ["application/json; q=1.0, text/html; q=0.",true, false]; + yield return ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", true, true]; + + + yield return ["", false, true]; + yield return ["text/", false, false]; + yield return ["text/*", false, true]; + yield return ["*/*", false, true]; + yield return ["application/json; q=1.0, */*; q=0.8", false, false]; // GO middleware says true + yield return ["text/html", false, true]; + yield return ["something/else", false, false]; + yield return ["text/html; q=1", false, true]; + yield return ["text/html; q=1.0", false, true]; + yield return ["text/html; q=0.9", false, true]; + yield return ["text/html; q=0", false, true]; // GO middleware says false + yield return ["text/html; q=0.0", false, true]; // GO middleware says false + yield return ["application/json", false, false]; + yield return ["application/json; q=1.0, text/html; q=0.9", false, false]; // GO middleware says true + yield return ["application/json; q=1.0, text/html; q=0", false, false]; + yield return ["application/json; q=0.9, text/html; q=1.0", false, true]; + yield return ["application/json; q=1.0, text/html; q=0.", false, false]; + yield return ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3", false, true]; } [DynamicData(nameof(GetTestNoAuthSessionIdAndGetRequestAndAcceptHeaderIsData), DynamicDataSourceType.Method, DynamicDataDisplayName = "DisplayName")] [DataTestMethod] - public async Task GetTestNoAuthSessionIdAndGetRequestAndAcceptHeaderIs(string acceptHeader, bool redirectExpected) + public async Task GetTestNoAuthSessionIdAndGetRequestAndAcceptHeaderIs(string acceptHeader,bool anonymousAllowed, bool redirectExpected) { + Console.WriteLine(acceptHeader); var requestHeader = new Dictionary{{"Accept", acceptHeader}}; - await TestMiddleWare("GET",requestHeader,"/a/b?q1=x&q2=1",redirectExpected?302:401,new Dictionary(),false ).ConfigureAwait(false); + await TestMiddleWare("GET", + requestHeader, + "/a/b?q1=x&q2=1", + anonymousAllowed, + redirectExpected?302:401, + new Dictionary(), + false ).ConfigureAwait(false); } - private async Task TestMiddleWare(string requestMethod, Dictionary requestHeader, string requestUri, int expectedStatus, Dictionary expectedResponseHeader,bool allowExternalValidation) + private async Task TestMiddleWare(string requestMethod, Dictionary requestHeader, string requestUri, bool allowAnonymous, int expectedStatus, Dictionary expectedResponseHeader,bool allowExternalValidation) { var uri = new Uri(requestUri, UriKind.RelativeOrAbsolute); if (!uri.IsAbsoluteUri) @@ -154,9 +239,15 @@ private async Task TestMiddleWare(string requestMethod, Dictionary mh.Send(It.IsAny())).Returns(new HttpResponseMessage() { StatusCode = HttpStatusCode.OK, - Content = new StringContent("") + Content = new StringContent(""), }); var client = new HttpClient(_fakeMessageHandler.Object); @@ -183,6 +274,15 @@ private async Task TestMiddleWare(string requestMethod, Dictionary(feature); + var endpointFeatureMock = new Mock(); + + endpointFeatureMock.SetupGet(endpointFeature => endpointFeature.Endpoint) + .Returns(new RouteEndpoint(_ => Task.CompletedTask, + RoutePatternFactory.Parse("/"), 0, + new EndpointMetadataCollection(allowAnonymous?new AllowAnonymousAttribute():new AuthorizeAttribute()), "Dummy")); + + context.Features.Set(endpointFeatureMock.Object); + async Task Next(HttpContext ctx) { Console.WriteLine(ctx.Response.Headers.Count); @@ -213,41 +313,38 @@ async Task Next(HttpContext ctx) //Used by Tests to create a readable TestName public static string DisplayName(MethodInfo methodInfo, object[] data) { - var displayName = $"{data[0]}"; + var displayName = $"{data[0]} ({string.Join( ", ", data.Skip(1) )})"; return string.IsNullOrWhiteSpace(displayName) ? "-" : displayName; } - private class MiddlewareMock + [Authorize] + private class MiddlewareMock(RequestDelegate next) { - private readonly RequestDelegate _next; public bool HasBeenInvoked { get; private set; } - - public MiddlewareMock(RequestDelegate next) - { - _next = next; - } public async Task InvokeAsync(HttpContext context) { - HasBeenInvoked = true; - context.Response.Body = new MemoryStream(); - context.Response.StatusCode = 401; - - await _next(context).ConfigureAwait(false); + try + { + HasBeenInvoked = true; + context.Response.Body = new MemoryStream(); + context.Response.StatusCode = 401; + + await next(context).ConfigureAwait(false); + } catch (Exception e) + { + Console.WriteLine(e); + throw; + } } } private class MockResponseFeature : IHttpResponseFeature { - public MockResponseFeature() - { - Headers = new HeaderDictionary(); - } - public Stream Body { get; set; } public bool HasStarted { get; private set; } - public IHeaderDictionary Headers { get; set; } + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); public string ReasonPhrase { get; set; } diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderSessionStoreTest.cs b/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderSessionStoreTest.cs index 47b1f83..728957d 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderSessionStoreTest.cs +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware.UnitTest/IdentityProviderSessionStoreTest.cs @@ -11,12 +11,12 @@ namespace Dvelop.Sdk.IdentityProviderMiddleware.UnitTest public class IdentityProviderSessionStoreTest { private IdentityProviderSessionStore _unit; - private Mock _clock; + private Mock _clock; [TestInitialize] public void Setup() { - _clock = new Mock {CallBase = true}; + _clock = new Mock {CallBase = true}; _unit = new IdentityProviderSessionStore(_clock.Object, 2); } @@ -63,7 +63,7 @@ public void GetSecondSessionUserCookieShouldReturnPrincipal() public void GetExpiredItemShouldReturnNull() { var now = DateTimeOffset.UtcNow; - _clock.SetupSequence(c => c.UtcNow) + _clock.SetupSequence(c => c.GetUtcNow()) .Returns(now) .Returns(now.AddMinutes(61)); @@ -82,7 +82,7 @@ public void GetExpiredItemShouldReturnNull() public void GetNonExpiredItemShouldReturn() { var now = DateTimeOffset.UtcNow; - _clock.SetupSequence(c => c.UtcNow) + _clock.SetupSequence(c => c.GetUtcNow()) .Returns(now).Returns(now) // SET .Returns(now.AddMinutes(61)) // GET a&1 .Returns(now.AddMinutes(61)); // GET b&1 diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationExtension.cs b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationExtension.cs index 8270a23..1f29db8 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationExtension.cs +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationExtension.cs @@ -7,7 +7,7 @@ public static class CustomAuthenticationExtensions { public static AuthenticationBuilder AddIdentityProviderAuthentication(this AuthenticationBuilder builder, string authenticationScheme, string displayName, Action configureOptions) { - return builder.AddScheme(authenticationScheme, displayName, configureOptions); + return builder.AddScheme(authenticationScheme, displayName, configureOptions); } } diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationHandler.cs b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationHandler.cs deleted file mode 100644 index 3bf3d67..0000000 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/CustomAuthenticationHandler.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Security.Claims; -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace Dvelop.Sdk.IdentityProvider.Middleware -{ - public class CustomAuthenticationHandler : AuthenticationHandler - { - public CustomAuthenticationHandler(IOptionsMonitor options, ILoggerFactory logger, - UrlEncoder encoder, ISystemClock clock) - : base(options, logger, encoder, clock) - { - } - - protected override Task HandleAuthenticateAsync() - { - return Task.FromResult( - AuthenticateResult.Success( - new AuthenticationTicket( - new ClaimsPrincipal(Options.Identity), - new AuthenticationProperties(), - Scheme.Name))); - } - } -} \ No newline at end of file diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.cs b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.cs index a1e04af..3fa58f8 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.cs +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.cs @@ -5,12 +5,13 @@ using System.Net.Http.Headers; using System.Threading.Tasks; using Dvelop.Sdk.IdentityProvider.Client; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; namespace Dvelop.Sdk.IdentityProvider.Middleware { - public class IdentityProviderMiddleware + public class IdentityProviderMiddleware { private readonly IdentityProviderClient _identityProviderClient; private readonly RequestDelegate _next; @@ -30,10 +31,10 @@ public IdentityProviderMiddleware(RequestDelegate next, IdentityProviderOptions TenantInformationCallback = clientOptions.TenantInformationCallback, UseMinimizedOnlyIdValidateDetailLevel = clientOptions.UseMinimizedOnlyIdValidateDetailLevel }; - - _identityProviderClient = new IdentityProviderClient( co ); + + _identityProviderClient = new IdentityProviderClient(co); } - + public async Task Invoke(HttpContext context) { var sessionId = context.GetAuthSessionId(); @@ -43,42 +44,44 @@ public async Task Invoke(HttpContext context) { context.User = await _identityProviderClient.GetClaimsPrincipalAsync(sessionId).ConfigureAwait(false); } - context.Response.OnStarting(state => + + var endpoint = context.GetEndpoint(); + var anon = endpoint?.Metadata?.GetMetadata(); + if (anon != null && context.Response.StatusCode == 0) { - if (context.Response.StatusCode != (int) HttpStatusCode.Unauthorized) - { - return Task.FromResult(false); - } - return Task.FromResult(RequestRedirectedToLogin(context, bearerTokenReceived)); - }, context.Response); - + context.Response.StatusCode = (int)HttpStatusCode.OK; + } + + context.Response.OnStarting( + _ => Task.FromResult(context.Response.StatusCode == (int)HttpStatusCode.Unauthorized && + RequestRedirectedToLogin(context)), context.Response); + await _next.Invoke(context).ConfigureAwait(false); } - - private bool RequestRedirectedToLogin(HttpContext context, bool bearerTokenReceived) + + private bool RequestRedirectedToLogin(HttpContext context) { - if(context == null) { throw new ArgumentNullException(nameof(context));} - - if (!string.IsNullOrWhiteSpace(context.User?.Identity?.Name)) + ArgumentNullException.ThrowIfNull(context); + + if (!string.IsNullOrWhiteSpace(context.User.Identity?.Name)) { return false; } - + if (HandleUnauthorizedRequest(context)) { return true; } - + var encodedUrl = context.Request.GetEncodedPathAndQuery(); context.Response.Redirect(_identityProviderClient.GetLoginUri(encodedUrl).ToString()); - return true; } private static bool HandleUnauthorizedRequest(HttpContext context) { - if(context== null) { throw new ArgumentNullException(nameof(context));} + ArgumentNullException.ThrowIfNull(context); var accept = context.Request.Headers["accept"]; var mediaTypeWithQualityHeaderValue = GetMediaTypes(accept)?.FirstOrDefault(); @@ -89,28 +92,26 @@ private static bool HandleUnauthorizedRequest(HttpContext context) mediaTypeWithQualityHeaderValue.MediaType != "*/*" && mediaTypeWithQualityHeaderValue.MediaType != "") { - context.Response.Headers?.Add("WWW-Authenticate", "Bearer"); - context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; + context.Response.Headers["WWW-Authenticate"] = "Bearer"; + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return true; } } if (context.Request.Method == "GET" || context.Request.Method == "HEAD") return false; - context.Response.Headers?.Add("WWW-Authenticate", "Bearer"); - context.Response.StatusCode = (int) HttpStatusCode.Unauthorized; + context.Response.Headers["WWW-Authenticate"] = "Bearer"; + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; return true; - } - - + private static IEnumerable GetMediaTypes(string headerValues) { if (string.IsNullOrEmpty(headerValues)) { return new List(); } - + return headerValues.Split(',') .Select(headerValue => { @@ -119,7 +120,7 @@ private static IEnumerable GetMediaTypes(string : new MediaTypeWithQualityHeaderValue("application/octed-stream"); return x; }) - .Where(h => h?.Quality.GetValueOrDefault(1) > 0) + .Where(h => h.Quality.GetValueOrDefault(1) > 0) .OrderByDescending(mt => mt.Quality.GetValueOrDefault(1)); } } diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.csproj b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.csproj index 1c98df5..cbc1cde 100644 --- a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.csproj +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdentityProviderMiddleware.csproj @@ -1,17 +1,21 @@ - + - netstandard2.0 + net8.0 Dvelop.Sdk.IdentityProvider.Middleware Dvelop.Sdk.IdentityProvider.Middleware true true true snupkg + latestmajor + Library + true - + + diff --git a/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdpAuthenticationHandler.cs b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdpAuthenticationHandler.cs new file mode 100644 index 0000000..5eb540f --- /dev/null +++ b/dvelop-sdk-identityprovider/IdentityProviderMiddleware/IdpAuthenticationHandler.cs @@ -0,0 +1,26 @@ +using System; +using System.Security.Claims; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Dvelop.Sdk.IdentityProvider.Middleware +{ + public class IdpAuthenticationHandler( + IOptionsMonitor options, + ILoggerFactory logger, + UrlEncoder encoder) + : AuthenticationHandler(options, logger, encoder) + { + + protected override Task HandleAuthenticateAsync() + { + var isIdpAuthenticated = Context.User.Identity?.AuthenticationType?.Equals("d.velop.IdentityProvider") ?? false; + return Task.FromResult(isIdpAuthenticated + ? AuthenticateResult.Success(new AuthenticationTicket(Context.User, new AuthenticationProperties(), Scheme.Name)) + : AuthenticateResult.NoResult()); + } + } +} \ No newline at end of file diff --git a/dvelop-sdk-logging/Logging.Abstractions/Logging.Abstractions.csproj b/dvelop-sdk-logging/Logging.Abstractions/Logging.Abstractions.csproj index d99a155..2517020 100644 --- a/dvelop-sdk-logging/Logging.Abstractions/Logging.Abstractions.csproj +++ b/dvelop-sdk-logging/Logging.Abstractions/Logging.Abstractions.csproj @@ -1,10 +1,10 @@  - netstandard2.0 + net8.0 Dvelop.Sdk.Logging.Abstractions Dvelop.Sdk.Logging.Abstractions - 8 + latestmajor true true @@ -12,11 +12,11 @@ - + - + diff --git a/dvelop-sdk-logging/Logging.OtelJsonConsole/Logging.OtelJsonConsole.csproj b/dvelop-sdk-logging/Logging.OtelJsonConsole/Logging.OtelJsonConsole.csproj index 2d9fb65..0ad5190 100644 --- a/dvelop-sdk-logging/Logging.OtelJsonConsole/Logging.OtelJsonConsole.csproj +++ b/dvelop-sdk-logging/Logging.OtelJsonConsole/Logging.OtelJsonConsole.csproj @@ -1,10 +1,10 @@ - netstandard2.1 + net8.0 Dvelop.Sdk.Logging.OtelJsonConsole Dvelop.Sdk.Logging.OtelJsonConsole - 8 + latestmajor true true @@ -12,12 +12,12 @@ - + - - + + diff --git a/dvelop-sdk-signing/SigningAlgorithms.UnitTest/SigningAlgorithms.UnitTest.csproj b/dvelop-sdk-signing/SigningAlgorithms.UnitTest/SigningAlgorithms.UnitTest.csproj index f9e719d..ceab483 100644 --- a/dvelop-sdk-signing/SigningAlgorithms.UnitTest/SigningAlgorithms.UnitTest.csproj +++ b/dvelop-sdk-signing/SigningAlgorithms.UnitTest/SigningAlgorithms.UnitTest.csproj @@ -6,14 +6,15 @@ Dvelop.Sdk.SigningAlgorithms.UnitTest Dvelop.Sdk.SigningAlgorithms.UnitTest Library + latestmajor - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/dvelop-sdk-signing/SigningAlgorithms/SigningAlgorithms.csproj b/dvelop-sdk-signing/SigningAlgorithms/SigningAlgorithms.csproj index 3fa121f..df26703 100644 --- a/dvelop-sdk-signing/SigningAlgorithms/SigningAlgorithms.csproj +++ b/dvelop-sdk-signing/SigningAlgorithms/SigningAlgorithms.csproj @@ -2,25 +2,25 @@ true - netstandard2.0 + net8.0 Dvelop.Sdk.SigningAlgorithms Dvelop.Sdk.SigningAlgorithms - - true + latestmajor + true true snupkg - + - - + + diff --git a/dvelop-sdk-tenant/TenantMiddleware.UnitTest/TenantMiddleware.UnitTest.csproj b/dvelop-sdk-tenant/TenantMiddleware.UnitTest/TenantMiddleware.UnitTest.csproj index 7ae5a03..8780652 100644 --- a/dvelop-sdk-tenant/TenantMiddleware.UnitTest/TenantMiddleware.UnitTest.csproj +++ b/dvelop-sdk-tenant/TenantMiddleware.UnitTest/TenantMiddleware.UnitTest.csproj @@ -6,15 +6,16 @@ Dvelop.Sdk.TenantMiddleware.UnitTest Dvelop.Sdk.TenantMiddleware.UnitTest Library + latestmajor - + - - - - + + + + diff --git a/dvelop-sdk-tenant/TenantMiddleware/TenantMiddleware.csproj b/dvelop-sdk-tenant/TenantMiddleware/TenantMiddleware.csproj index a12af6e..150c64e 100644 --- a/dvelop-sdk-tenant/TenantMiddleware/TenantMiddleware.csproj +++ b/dvelop-sdk-tenant/TenantMiddleware/TenantMiddleware.csproj @@ -2,7 +2,7 @@ Dvelop.Sdk.TenantMiddleware - netstandard2.0 + net8.0 Dvelop.Sdk.TenantMiddleware Dvelop.Sdk.TenantMiddleware Copyright (c) 2019 @@ -19,7 +19,7 @@ - + diff --git a/dvelop-sdk-webapi/WebApiExtensions.UnitTest/WebApiExtensions.UnitTest.csproj b/dvelop-sdk-webapi/WebApiExtensions.UnitTest/WebApiExtensions.UnitTest.csproj index 84d7a36..ced8cf6 100644 --- a/dvelop-sdk-webapi/WebApiExtensions.UnitTest/WebApiExtensions.UnitTest.csproj +++ b/dvelop-sdk-webapi/WebApiExtensions.UnitTest/WebApiExtensions.UnitTest.csproj @@ -10,14 +10,16 @@ Dvelop.Sdk.WebApiExtensions.UnitTest Library + + latestmajor - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/dvelop-sdk-webapi/WebApiExtensions/WebApiExtensions.csproj b/dvelop-sdk-webapi/WebApiExtensions/WebApiExtensions.csproj index a10d70f..8324a90 100644 --- a/dvelop-sdk-webapi/WebApiExtensions/WebApiExtensions.csproj +++ b/dvelop-sdk-webapi/WebApiExtensions/WebApiExtensions.csproj @@ -1,10 +1,10 @@ - netstandard2.0 + net8.0 Dvelop.Sdk.WebApiExtensions Dvelop.Sdk.WebApiExtensions - 8 + latestmajor true true @@ -12,8 +12,8 @@ - - + +