From b34ac576573313f61ef18350a6ba21929bec3309 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 10:58:08 +0200 Subject: [PATCH 01/19] feat: enable nullable in Sinch.csproj --- src/Sinch/Sinch.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sinch/Sinch.csproj b/src/Sinch/Sinch.csproj index c11672a2..7018e9a5 100644 --- a/src/Sinch/Sinch.csproj +++ b/src/Sinch/Sinch.csproj @@ -1,7 +1,7 @@  - disable + enable net6.0;net7.0;net8.0 https://github.com/sinch/sinch-sdk-dotnet 0.1.0 From ee57dc109cddc3132c58453f3a68432405fcf6b1 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:02:10 +0200 Subject: [PATCH 02/19] refactor: use nullable in SinchClient.cs --- src/Sinch/SinchClient.cs | 21 +++++++++++---------- src/Sinch/SinchOptions.cs | 24 ++++++++++++------------ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/Sinch/SinchClient.cs b/src/Sinch/SinchClient.cs index 8ab8c0d1..ffc2bbff 100644 --- a/src/Sinch/SinchClient.cs +++ b/src/Sinch/SinchClient.cs @@ -114,7 +114,7 @@ public ISinchVerificationClient Verification(string appKey, string appSecret, /// Defaults to Application Sign request and it's a recommended approach. /// /// - public ISinchVoiceClient Voice(string appKey, string appSecret, CallingRegion callingRegion = null, + public ISinchVoiceClient Voice(string appKey, string appSecret, CallingRegion? callingRegion = null, AuthStrategy authStrategy = AuthStrategy.ApplicationSign); } @@ -129,20 +129,21 @@ public class SinchClient : ISinchClient private const string AuthApiUrl = "https://auth.sinch.com"; private const string TemplatesApiUrlTemplate = "https://{0}.template.api.sinch.com/"; - private readonly ApiUrlOverrides _apiUrlOverrides; + private readonly ApiUrlOverrides? _apiUrlOverrides; private readonly ISinchAuth _auth; private readonly ISinchConversation _conversation; private readonly HttpClient _httpClient; - private readonly string _keyId; - private readonly string _keySecret; - + private readonly string? _keyId; + private readonly string? _keySecret; + private readonly string? _projectId; + private readonly LoggerFactory _loggerFactory; private readonly ISinchNumbers _numbers; - private readonly string _projectId; + private readonly ISinchSms _sms; - private readonly ILoggerAdapter _logger; + private readonly ILoggerAdapter? _logger; /// /// Initialize a new @@ -152,8 +153,8 @@ public class SinchClient : ISinchClient /// Your project id. /// Optional. See: /// - public SinchClient(string projectId, string keyId, string keySecret, - Action options = default) + public SinchClient(string? projectId, string? keyId, string? keySecret, + Action? options = default) { _projectId = projectId; _keyId = keyId; @@ -269,7 +270,7 @@ public ISinchVerificationClient Verification(string appKey, string appSecret, /// public ISinchVoiceClient Voice(string appKey, string appSecret, - CallingRegion callingRegion = default, + CallingRegion? callingRegion = default, AuthStrategy authStrategy = AuthStrategy.ApplicationSign) { if (string.IsNullOrEmpty(appKey)) diff --git a/src/Sinch/SinchOptions.cs b/src/Sinch/SinchOptions.cs index 7cefa3a8..1a0978e2 100644 --- a/src/Sinch/SinchOptions.cs +++ b/src/Sinch/SinchOptions.cs @@ -11,13 +11,13 @@ public sealed class SinchOptions /// /// A logger factory used to create ILogger inside the SDK to enable logging /// - public ILoggerFactory LoggerFactory { get; set; } + public ILoggerFactory? LoggerFactory { get; set; } /// /// A HttpClient to use. If not provided, HttpClient will be created and managed by /// itself /// - public HttpClient HttpClient { get; set; } + public HttpClient? HttpClient { get; set; } /// /// Set's the hosting region for the SMS service. @@ -38,10 +38,10 @@ public sealed class SinchOptions public ConversationRegion ConversationRegion { get; set; } = ConversationRegion.Us; /// - public ApiUrlOverrides ApiUrlOverrides { get; set; } + public ApiUrlOverrides? ApiUrlOverrides { get; set; } - internal ServicePlanIdOptions ServicePlanIdOptions { get; private set; } + internal ServicePlanIdOptions? ServicePlanIdOptions { get; private set; } /// /// Use SMS API with `service plan id` and compatible region. @@ -52,7 +52,7 @@ public sealed class SinchOptions /// Region to use. Defaults to /// throws if service plan id or region is null or an empty string public void UseServicePlanIdWithSms(string servicePlanId, - string apiToken, SmsServicePlanIdHostingRegion hostingRegion = default) + string apiToken, SmsServicePlanIdHostingRegion? hostingRegion = default) { hostingRegion ??= SmsServicePlanIdHostingRegion.Us; @@ -99,37 +99,37 @@ public sealed class ApiUrlOverrides /// /// Overrides SMS api base url /// - public string SmsUrl { get; init; } + public string? SmsUrl { get; init; } /// /// Overrides Conversation api base url /// - public string ConversationUrl { get; init; } + public string? ConversationUrl { get; init; } /// /// Overrides Templates api base url. /// Templates is treated as part of conversation api, but it has another base address. /// - public string TemplatesUrl { get; init; } + public string? TemplatesUrl { get; init; } /// /// Overrides Voice api base url /// - public string VoiceUrl { get; init; } + public string? VoiceUrl { get; init; } /// /// Overrides Verification api base url /// - public string VerificationUrl { get; init; } + public string? VerificationUrl { get; init; } /// /// Overrides Auth api base url /// - public string AuthUrl { get; init; } + public string? AuthUrl { get; init; } /// /// Overrides Numbers api base url /// - public string NumbersUrl { get; init; } + public string? NumbersUrl { get; init; } } } From 851d0f73b6e64d7cb02220bab4c847e4d59d1a94 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:07:20 +0200 Subject: [PATCH 03/19] refactor: enable nullable references --- src/Sinch/ApiError.cs | 12 ++++++------ src/Sinch/Core/JsonExtensions.cs | 2 +- src/Sinch/Core/JsonInterfaceConverter.cs | 24 ++++++++++++++---------- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/Sinch/ApiError.cs b/src/Sinch/ApiError.cs index 122e0600..fc21a46f 100644 --- a/src/Sinch/ApiError.cs +++ b/src/Sinch/ApiError.cs @@ -5,21 +5,21 @@ namespace Sinch { internal sealed class ApiErrorResponse { - public ApiError Error { get; set; } + public ApiError? Error { get; set; } - public string Code { get; set; } + public string? Code { get; set; } - public string Text { get; set; } + public string? Text { get; set; } } internal sealed class ApiError { public int Code { get; set; } - public string Message { get; set; } + public string? Message { get; set; } - public string Status { get; set; } + public string? Status { get; set; } - public List Details { get; set; } + public List? Details { get; set; } } } diff --git a/src/Sinch/Core/JsonExtensions.cs b/src/Sinch/Core/JsonExtensions.cs index f99ab0ed..cbab004a 100644 --- a/src/Sinch/Core/JsonExtensions.cs +++ b/src/Sinch/Core/JsonExtensions.cs @@ -9,7 +9,7 @@ namespace Sinch.Core { public static class JsonExtensions { - public static object ToObject(this JsonElement element, Type type, JsonSerializerOptions options = null) + public static object? ToObject(this JsonElement element, Type type, JsonSerializerOptions? options = null) { var bufferWriter = new ArrayBufferWriter(); using (var writer = new Utf8JsonWriter(bufferWriter)) diff --git a/src/Sinch/Core/JsonInterfaceConverter.cs b/src/Sinch/Core/JsonInterfaceConverter.cs index cfa85a6e..3334c7be 100644 --- a/src/Sinch/Core/JsonInterfaceConverter.cs +++ b/src/Sinch/Core/JsonInterfaceConverter.cs @@ -13,9 +13,9 @@ public JsonInterfaceConverterAttribute(Type convertedType) : base(convertedType) } } - public class InterfaceConverter : JsonConverter where T : class + public class InterfaceConverter : JsonConverter where T : class { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override T? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { var type = typeof(T); var types = AppDomain.CurrentDomain.GetAssemblies() @@ -23,21 +23,25 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial .Where(p => type.IsAssignableFrom(p)); var elem = JsonElement.ParseValue(ref reader); - dynamic obj = null; + dynamic? obj = null; foreach (var @class in types) { - if (elem.IsTypeOf(@class, options)) - { - obj = elem.ToObject(@class, options); - break; - } + if (!elem.IsTypeOf(@class, options)) continue; + + obj = elem.ToObject(@class, options); + break; } - return (T)obj; + return (T?)obj; } - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { + if (value is null) + { + throw new NullReferenceException("value is null"); + } + var type = value.GetType(); JsonSerializer.Serialize(writer, value, type, options); } From 0b5d7a2e17340cf729e60b85c50fac47f7b3561d Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:09:25 +0200 Subject: [PATCH 04/19] refactor: update example --- examples/Console/UseServicePlanIdForSms.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/Console/UseServicePlanIdForSms.cs b/examples/Console/UseServicePlanIdForSms.cs index a4ed1934..ca00942b 100644 --- a/examples/Console/UseServicePlanIdForSms.cs +++ b/examples/Console/UseServicePlanIdForSms.cs @@ -11,8 +11,8 @@ public void Example() var sinchClient = new SinchClient(default, default, default, options => { - options.UseServicePlanIdWithSms(Environment.GetEnvironmentVariable("SINCH_SERVICE_PLAN_ID"), - Environment.GetEnvironmentVariable("SINCH_API_TOKEN"), SmsServicePlanIdHostingRegion.Ca); + options.UseServicePlanIdWithSms(Environment.GetEnvironmentVariable("SINCH_SERVICE_PLAN_ID")!, + Environment.GetEnvironmentVariable("SINCH_API_TOKEN")!, SmsServicePlanIdHostingRegion.Ca); }); sinchClient.Sms.Batches.Send(new SendTextBatchRequest() { From 624a1914d387782f91182bf99c52c617fb045a08 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:20:00 +0200 Subject: [PATCH 05/19] refactor: refs?(will refer with this to nullable ref types), use IHttp in loggers --- src/Sinch/Core/Http.cs | 4 ++-- src/Sinch/SinchApiException.cs | 9 +++++---- src/Sinch/SinchClient.cs | 31 +++++++++++++++++-------------- 3 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/Sinch/Core/Http.cs b/src/Sinch/Core/Http.cs index edabb90d..df21faa2 100644 --- a/src/Sinch/Core/Http.cs +++ b/src/Sinch/Core/Http.cs @@ -52,12 +52,12 @@ internal class Http : IHttp { private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly ILoggerAdapter _logger; + private readonly ILoggerAdapter _logger; private readonly ISinchAuth _auth; private readonly string _userAgentHeaderValue; - public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter logger, + public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter logger, JsonNamingPolicy jsonNamingPolicy) { _logger = logger; diff --git a/src/Sinch/SinchApiException.cs b/src/Sinch/SinchApiException.cs index 8e8ad22b..41a205f3 100644 --- a/src/Sinch/SinchApiException.cs +++ b/src/Sinch/SinchApiException.cs @@ -1,7 +1,7 @@ using System; +using System.Collections.Generic; using System.Net; using System.Net.Http; -using System.Collections.Generic; using System.Text.Json.Nodes; namespace Sinch @@ -17,7 +17,8 @@ private SinchApiException(string message, Exception inner, HttpStatusCode status Details = new List(); } - internal SinchApiException(HttpStatusCode statusCode, string message, Exception inner, ApiErrorResponse authApiError) + internal SinchApiException(HttpStatusCode statusCode, string message, Exception inner, + ApiErrorResponse authApiError) : this($"{message}:{authApiError?.Error?.Message ?? authApiError?.Text}", inner, statusCode) { // https://developers.sinch.com/docs/sms/api-reference/status-codes/#4xx---user-errors @@ -29,9 +30,9 @@ internal SinchApiException(HttpStatusCode statusCode, string message, Exception Details = details?.Details ?? new List(); } - public string DetailedMessage { get; init; } + public string? DetailedMessage { get; init; } - public string Status { get; init; } + public string? Status { get; init; } public List Details { get; init; } } diff --git a/src/Sinch/SinchClient.cs b/src/Sinch/SinchClient.cs index ffc2bbff..fd4e84fe 100644 --- a/src/Sinch/SinchClient.cs +++ b/src/Sinch/SinchClient.cs @@ -137,11 +137,11 @@ public class SinchClient : ISinchClient private readonly string? _keyId; private readonly string? _keySecret; private readonly string? _projectId; - + private readonly LoggerFactory _loggerFactory; private readonly ISinchNumbers _numbers; - + private readonly ISinchSms _sms; private readonly ILoggerAdapter? _logger; @@ -176,18 +176,18 @@ public SinchClient(string? projectId, string? keyId, string? keySecret, _httpClient = optionsObj.HttpClient ?? new HttpClient(); - _apiUrlOverrides = optionsObj?.ApiUrlOverrides; + _apiUrlOverrides = optionsObj.ApiUrlOverrides; ISinchAuth auth = - new OAuth(_keyId, _keySecret, _httpClient, _loggerFactory?.Create(), + new OAuth(_keyId!, _keySecret!, _httpClient, _loggerFactory?.Create(), new Uri(_apiUrlOverrides?.AuthUrl ?? AuthApiUrl)); _auth = auth; - var httpCamelCase = new Http(auth, _httpClient, _loggerFactory?.Create(), + var httpCamelCase = new Http(auth, _httpClient, _loggerFactory?.Create(), JsonNamingPolicy.CamelCase); - var httpSnakeCaseOAuth = new Http(auth, _httpClient, _loggerFactory?.Create(), + var httpSnakeCaseOAuth = new Http(auth, _httpClient, _loggerFactory?.Create(), SnakeCaseNamingPolicy.Instance); - _numbers = new Numbers.Numbers(_projectId, new Uri(_apiUrlOverrides?.NumbersUrl ?? NumbersApiUrl), + _numbers = new Numbers.Numbers(_projectId!, new Uri(_apiUrlOverrides?.NumbersUrl ?? NumbersApiUrl), _loggerFactory, httpCamelCase); _sms = InitSms(optionsObj, httpSnakeCaseOAuth); @@ -198,7 +198,7 @@ public SinchClient(string? projectId, string? keyId, string? keySecret, var templatesBaseAddress = new Uri(_apiUrlOverrides?.TemplatesUrl ?? string.Format(TemplatesApiUrlTemplate, optionsObj.ConversationRegion.Value)); - _conversation = new SinchConversationClient(_projectId, conversationBaseAddress + _conversation = new SinchConversationClient(_projectId!, conversationBaseAddress , templatesBaseAddress, _loggerFactory, httpSnakeCaseOAuth); @@ -263,7 +263,7 @@ public ISinchVerificationClient Verification(string appKey, string appSecret, else auth = new BasicAuth(appKey, appSecret); - var http = new Http(auth, _httpClient, _loggerFactory?.Create(), JsonNamingPolicy.CamelCase); + var http = new Http(auth, _httpClient, _loggerFactory?.Create(), JsonNamingPolicy.CamelCase); return new SinchVerificationClient(new Uri(_apiUrlOverrides?.VerificationUrl ?? VerificationApiUrl), _loggerFactory, http); } @@ -287,7 +287,7 @@ public ISinchVoiceClient Voice(string appKey, string appSecret, callingRegion ??= CallingRegion.Global; - var http = new Http(auth, _httpClient, _loggerFactory?.Create(), JsonNamingPolicy.CamelCase); + var http = new Http(auth, _httpClient, _loggerFactory?.Create(), JsonNamingPolicy.CamelCase); return new SinchVoiceClient( new Uri(_apiUrlOverrides?.VoiceUrl ?? string.Format(VoiceApiUrlTemplate, callingRegion.Value)), _loggerFactory, http); @@ -316,7 +316,7 @@ private SmsClient InitSms(SinchOptions optionsObj, IHttp httpSnakeCase) optionsObj.ServicePlanIdOptions.ServicePlanId, optionsObj.ServicePlanIdOptions.HostingRegion.Value); var bearerSnakeHttp = new Http(new BearerAuth(optionsObj.ServicePlanIdOptions.ApiToken), _httpClient, - _loggerFactory?.Create(), + _loggerFactory?.Create(), SnakeCaseNamingPolicy.Instance); return new SmsClient(new ServicePlanId(optionsObj.ServicePlanIdOptions.ServicePlanId), BuildServicePlanIdSmsBaseAddress(optionsObj.ServicePlanIdOptions.HostingRegion, @@ -326,14 +326,17 @@ private SmsClient InitSms(SinchOptions optionsObj, IHttp httpSnakeCase) _logger?.LogInformation("Initializing SMS client with {project_id} in {region}", _projectId, optionsObj.SmsHostingRegion.Value); - return new SmsClient(new ProjectId(_projectId), + + return new SmsClient( + new ProjectId( + _projectId!), // exception is throw when trying to get SMS client property if _projectId is null BuildSmsBaseAddress(optionsObj.SmsHostingRegion, _apiUrlOverrides?.SmsUrl), _loggerFactory, httpSnakeCase); } private static Uri BuildServicePlanIdSmsBaseAddress(SmsServicePlanIdHostingRegion smsServicePlanIdHostingRegion, - string smsUrlOverride) + string? smsUrlOverride) { if (!string.IsNullOrEmpty(smsUrlOverride)) return new Uri(smsUrlOverride); @@ -341,7 +344,7 @@ private static Uri BuildServicePlanIdSmsBaseAddress(SmsServicePlanIdHostingRegio smsServicePlanIdHostingRegion.Value.ToLowerInvariant())); } - private static Uri BuildSmsBaseAddress(SmsHostingRegion smsHostingRegion, string smsUrlOverride) + private static Uri BuildSmsBaseAddress(SmsHostingRegion smsHostingRegion, string? smsUrlOverride) { if (!string.IsNullOrEmpty(smsUrlOverride)) return new Uri(smsUrlOverride); From e415368bc342b9538001e1abc335e82cbd05998b Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:20:33 +0200 Subject: [PATCH 06/19] refactor: cleanup using directive --- examples/Console/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/Console/Program.cs b/examples/Console/Program.cs index 1c92a896..edbe58d3 100644 --- a/examples/Console/Program.cs +++ b/examples/Console/Program.cs @@ -1,6 +1,5 @@ using DotNetEnv; using Sinch; -using Sinch.Auth; // Assume .env file is present in your output directory Env.Load(); From ef6ea4e1a62bab5f0c1fa62c08b763e0b6411914 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:32:24 +0200 Subject: [PATCH 07/19] chore: disable unused variables warning in examples --- examples/Console/.editorconfig | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 examples/Console/.editorconfig diff --git a/examples/Console/.editorconfig b/examples/Console/.editorconfig new file mode 100644 index 00000000..96857e89 --- /dev/null +++ b/examples/Console/.editorconfig @@ -0,0 +1,3 @@ +[*] +resharper_unused_variable_highlighting=none +resharper_unused_variable_compiler_highlighting=none \ No newline at end of file From 6907ae0ed1d0ecb317dca31f9c68660e870e7bad Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:33:42 +0200 Subject: [PATCH 08/19] fix: refs? --- src/Sinch/SinchApiException.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Sinch/SinchApiException.cs b/src/Sinch/SinchApiException.cs index 41a205f3..05429ec8 100644 --- a/src/Sinch/SinchApiException.cs +++ b/src/Sinch/SinchApiException.cs @@ -19,14 +19,14 @@ private SinchApiException(string message, Exception inner, HttpStatusCode status internal SinchApiException(HttpStatusCode statusCode, string message, Exception inner, ApiErrorResponse authApiError) - : this($"{message}:{authApiError?.Error?.Message ?? authApiError?.Text}", inner, statusCode) + : this($"{message}:{authApiError.Error?.Message ?? authApiError.Text}", inner, statusCode) { // https://developers.sinch.com/docs/sms/api-reference/status-codes/#4xx---user-errors // there can be nested error object or simple { text: "", code: "code" } not nested object with api errors // nested object takes precedence in fields population - var details = authApiError?.Error; - Status = details?.Status ?? authApiError?.Code; - DetailedMessage = details?.Message ?? authApiError?.Text; + var details = authApiError.Error; + Status = details?.Status ?? authApiError.Code; + DetailedMessage = details?.Message ?? authApiError.Text; Details = details?.Details ?? new List(); } From 024867fba5f15b5897a9850af3415eb9b06452b6 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:34:13 +0200 Subject: [PATCH 09/19] chore: add dictionary shared --- Sinch.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Sinch.sln.DotSettings diff --git a/Sinch.sln.DotSettings b/Sinch.sln.DotSettings new file mode 100644 index 00000000..ced70ca7 --- /dev/null +++ b/Sinch.sln.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file From 9778800812ff919984130e965de8d009887cab33 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:36:17 +0200 Subject: [PATCH 10/19] refs? --- src/Sinch/Auth/ApplicationSignedAuth.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Sinch/Auth/ApplicationSignedAuth.cs b/src/Sinch/Auth/ApplicationSignedAuth.cs index 87b172b2..6ebb81e5 100644 --- a/src/Sinch/Auth/ApplicationSignedAuth.cs +++ b/src/Sinch/Auth/ApplicationSignedAuth.cs @@ -9,11 +9,11 @@ internal class ApplicationSignedAuth : ISinchAuth { private readonly string _appSecret; private readonly string _appKey; - private byte[] _jsonBodyInBytes; - private string _httpVerb; - private string _requestContentType; - private string _requestPath; - private string _timestamp; + private byte[]? _jsonBodyInBytes; + private string? _httpVerb; + private string? _requestContentType; + private string? _requestPath; + private string? _timestamp; public string Scheme { get; } = AuthSchemes.Application; @@ -44,7 +44,7 @@ public Task GetAuthToken(bool force = false) encodedBody = Convert.ToBase64String(md5Bytes); } - var toSign = new StringBuilder().AppendJoin('\n', _httpVerb.ToUpperInvariant(), encodedBody, + var toSign = new StringBuilder().AppendJoin('\n', _httpVerb?.ToUpperInvariant(), encodedBody, _requestContentType, _timestamp, _requestPath).ToString(); From 6a2277bb6d6f75339b4d034adcb5fd33d10d83b0 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:43:38 +0200 Subject: [PATCH 11/19] refactor(auth): refs? --- src/Sinch/Auth/AuthApiError.cs | 8 ++++---- src/Sinch/Auth/OAuth.cs | 14 ++++++++++---- src/Sinch/Auth/SinchAuthException.cs | 20 ++++++++++---------- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/Sinch/Auth/AuthApiError.cs b/src/Sinch/Auth/AuthApiError.cs index f72c6c9d..a824d3a7 100644 --- a/src/Sinch/Auth/AuthApiError.cs +++ b/src/Sinch/Auth/AuthApiError.cs @@ -4,15 +4,15 @@ namespace Sinch.Auth { internal class AuthApiError { - public string Error { get; set; } + public string? Error { get; set; } [JsonPropertyName("error_verbose")] - public string ErrorVerbose { get; set; } + public string? ErrorVerbose { get; set; } [JsonPropertyName("error_description")] - public string ErrorDescription { get; set; } + public string? ErrorDescription { get; set; } [JsonPropertyName("error_hint")] - public string ErrorHint { get; set; } + public string? ErrorHint { get; set; } } } diff --git a/src/Sinch/Auth/OAuth.cs b/src/Sinch/Auth/OAuth.cs index 4e619c65..5791942d 100644 --- a/src/Sinch/Auth/OAuth.cs +++ b/src/Sinch/Auth/OAuth.cs @@ -16,13 +16,14 @@ internal class OAuth : ISinchAuth private readonly HttpClient _httpClient; private readonly string _keyId; private readonly string _keySecret; - private readonly ILoggerAdapter _logger; - private volatile string _token; + private readonly ILoggerAdapter? _logger; + private volatile string? _token; private readonly Uri _baseAddress; public string Scheme { get; } = AuthSchemes.Bearer; - public OAuth(string keyId, string keySecret, HttpClient httpClient, ILoggerAdapter logger, Uri baseAddress) + public OAuth(string keyId, string keySecret, HttpClient httpClient, ILoggerAdapter? logger, + Uri baseAddress) { _keyId = keyId; _keySecret = keySecret; @@ -80,7 +81,12 @@ public async Task GetAuthToken(bool force = false) private class AuthResponse { [JsonPropertyName("access_token")] - public string AccessToken { get; set; } +#if NET7_0_OR_GREATER + public required string AccessToken { get; set; } +#else + public string AccessToken { get; set; } = null!; +#endif + /// /// In seconds diff --git a/src/Sinch/Auth/SinchAuthException.cs b/src/Sinch/Auth/SinchAuthException.cs index cd0d5d11..2ee4eedc 100644 --- a/src/Sinch/Auth/SinchAuthException.cs +++ b/src/Sinch/Auth/SinchAuthException.cs @@ -6,26 +6,26 @@ namespace Sinch.Auth { public sealed class SinchAuthException : HttpRequestException { - private SinchAuthException(HttpStatusCode statusCode, string message, Exception inner) : base(message, inner, + private SinchAuthException(HttpStatusCode statusCode, string? message, Exception? inner) : base(message, inner, statusCode) { } - internal SinchAuthException(HttpStatusCode statusCode, string message, Exception inner, AuthApiError authApiError) + internal SinchAuthException(HttpStatusCode statusCode, string? message, Exception? inner, AuthApiError authApiError) : this(statusCode, message, inner) { - Error = authApiError?.Error; - ErrorDescription = authApiError?.ErrorDescription; - ErrorHint = authApiError?.ErrorHint; - ErrorVerbose = authApiError?.ErrorVerbose; + Error = authApiError.Error; + ErrorDescription = authApiError.ErrorDescription; + ErrorHint = authApiError.ErrorHint; + ErrorVerbose = authApiError.ErrorVerbose; } - public string Error { get; } + public string? Error { get; } - public string ErrorVerbose { get; } + public string? ErrorVerbose { get; } - public string ErrorDescription { get; } + public string? ErrorDescription { get; } - public string ErrorHint { get; } + public string? ErrorHint { get; } } } From e876e3ba4692d012407be74cceccca2a442cf00f Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 11:59:45 +0200 Subject: [PATCH 12/19] refactor: refs? --- src/Sinch/Auth/ApplicationSignedAuth.cs | 2 +- src/Sinch/Auth/SinchAuthException.cs | 10 ++++----- src/Sinch/Core/EnumRecord.cs | 6 ++++-- src/Sinch/Core/Extensions.cs | 2 +- src/Sinch/Core/Http.cs | 28 ++++++++++++++++--------- src/Sinch/SinchApiException.cs | 14 ++++++------- 6 files changed, 36 insertions(+), 26 deletions(-) diff --git a/src/Sinch/Auth/ApplicationSignedAuth.cs b/src/Sinch/Auth/ApplicationSignedAuth.cs index 6ebb81e5..4cd1926a 100644 --- a/src/Sinch/Auth/ApplicationSignedAuth.cs +++ b/src/Sinch/Auth/ApplicationSignedAuth.cs @@ -24,7 +24,7 @@ public ApplicationSignedAuth(string appKey, string appSecret) } public string GetSignedAuth(byte[] jsonBodyBytes, string httpVerb, - string requestPath, string timestamp, string contentType) + string requestPath, string timestamp, string? contentType) { _jsonBodyInBytes = jsonBodyBytes; _httpVerb = httpVerb; diff --git a/src/Sinch/Auth/SinchAuthException.cs b/src/Sinch/Auth/SinchAuthException.cs index 2ee4eedc..94cb6116 100644 --- a/src/Sinch/Auth/SinchAuthException.cs +++ b/src/Sinch/Auth/SinchAuthException.cs @@ -11,13 +11,13 @@ private SinchAuthException(HttpStatusCode statusCode, string? message, Exception { } - internal SinchAuthException(HttpStatusCode statusCode, string? message, Exception? inner, AuthApiError authApiError) + internal SinchAuthException(HttpStatusCode statusCode, string? message, Exception? inner, AuthApiError? authApiError) : this(statusCode, message, inner) { - Error = authApiError.Error; - ErrorDescription = authApiError.ErrorDescription; - ErrorHint = authApiError.ErrorHint; - ErrorVerbose = authApiError.ErrorVerbose; + Error = authApiError?.Error; + ErrorDescription = authApiError?.ErrorDescription; + ErrorHint = authApiError?.ErrorHint; + ErrorVerbose = authApiError?.ErrorVerbose; } public string? Error { get; } diff --git a/src/Sinch/Core/EnumRecord.cs b/src/Sinch/Core/EnumRecord.cs index 78c8e3fb..511c9acf 100644 --- a/src/Sinch/Core/EnumRecord.cs +++ b/src/Sinch/Core/EnumRecord.cs @@ -16,7 +16,8 @@ public class EnumRecordJsonConverter : JsonConverter where T : EnumRecord { public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return Activator.CreateInstance(typeToConvert, reader.GetString()) as T; + return Activator.CreateInstance(typeToConvert, reader.GetString()) as T ?? + throw new InvalidOperationException("Created instance is null"); } public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) @@ -28,7 +29,8 @@ public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions public override T ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return Activator.CreateInstance(typeToConvert, reader.GetString()) as T; + return Activator.CreateInstance(typeToConvert, reader.GetString()) as T ?? + throw new InvalidOperationException("Created instance is null"); } public override void WriteAsPropertyName(Utf8JsonWriter writer, T value, JsonSerializerOptions options) diff --git a/src/Sinch/Core/Extensions.cs b/src/Sinch/Core/Extensions.cs index 27bc3246..cc88b743 100644 --- a/src/Sinch/Core/Extensions.cs +++ b/src/Sinch/Core/Extensions.cs @@ -20,7 +20,7 @@ public static async Task EnsureSuccessApiStatusCode(this HttpResponseMessage htt throw new SinchApiException(httpResponseMessage.StatusCode, httpResponseMessage.ReasonPhrase, null, apiError); } - public static async Task TryGetJson(this HttpResponseMessage httpResponseMessage) + public static async Task TryGetJson(this HttpResponseMessage httpResponseMessage) { var authResponse = default(T); if (httpResponseMessage.IsJson()) authResponse = await httpResponseMessage.Content.ReadFromJsonAsync(); diff --git a/src/Sinch/Core/Http.cs b/src/Sinch/Core/Http.cs index df21faa2..dc96209a 100644 --- a/src/Sinch/Core/Http.cs +++ b/src/Sinch/Core/Http.cs @@ -30,7 +30,7 @@ internal interface IHttp /// /// The type of the response object. /// - Task Send(Uri uri, HttpMethod httpMethod, + Task Send(Uri uri, HttpMethod httpMethod, CancellationToken cancellationToken = default); /// @@ -43,7 +43,7 @@ Task Send(Uri uri, HttpMethod httpMethod, /// The type of the request object. /// The type of the response object. /// - Task Send(Uri uri, HttpMethod httpMethod, TRequest httpContent, + Task Send(Uri uri, HttpMethod httpMethod, TRequest httpContent, CancellationToken cancellationToken = default); } @@ -68,25 +68,25 @@ public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter logger PropertyNamingPolicy = jsonNamingPolicy, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; - var sdkVersion = new AssemblyName(typeof(Http).GetTypeInfo()!.Assembly!.FullName!).Version!.ToString(); + var sdkVersion = new AssemblyName(typeof(Http).GetTypeInfo().Assembly.FullName!).Version!.ToString(); _userAgentHeaderValue = $"sinch-sdk/{sdkVersion} (csharp/{RuntimeInformation.FrameworkDescription};;)"; } - public Task Send(Uri uri, HttpMethod httpMethod, + public Task Send(Uri uri, HttpMethod httpMethod, CancellationToken cancellationToken = default) { return Send(uri, httpMethod, null, cancellationToken); } - public async Task Send(Uri uri, HttpMethod httpMethod, TRequest request, + public async Task Send(Uri uri, HttpMethod httpMethod, TRequest? request, CancellationToken cancellationToken = default) { var retry = true; while (true) { _logger?.LogDebug("Sending request to {uri}", uri); - HttpContent httpContent = + HttpContent? httpContent = request == null ? null : JsonContent.Create(request, options: _jsonSerializerOptions); #if DEBUG @@ -108,10 +108,17 @@ public async Task Send(Uri uri, HttpMethod httpM var now = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture); const string headerName = "x-timestamp"; msg.Headers.Add(headerName, now); + + var bytes = Array.Empty(); + if (msg.Content is not null) + { + bytes = await msg.Content.ReadAsByteArrayAsync(cancellationToken); + } + token = appSignAuth.GetSignedAuth( - msg.Content?.ReadAsByteArrayAsync(cancellationToken).GetAwaiter().GetResult(), + bytes, msg.Method.ToString().ToUpperInvariant(), msg.RequestUri.PathAndQuery, - $"{headerName}:{now}", msg.Content?.Headers?.ContentType?.ToString()); + $"{headerName}:{now}", msg.Content?.Headers.ContentType?.ToString()); retry = false; } else @@ -130,8 +137,8 @@ public async Task Send(Uri uri, HttpMethod httpM { // will not retry when no "expired" header for a token. const string wwwAuthenticateHeader = "www-authenticate"; - if (_auth.Scheme == AuthSchemes.Bearer && true == result.Headers?.Contains(wwwAuthenticateHeader) && - false == result.Headers?.GetValues(wwwAuthenticateHeader)?.Contains("expired")) + if (_auth.Scheme == AuthSchemes.Bearer && result.Headers.Contains(wwwAuthenticateHeader) && + !result.Headers.GetValues(wwwAuthenticateHeader).Contains("expired")) { _logger?.LogDebug("OAuth Unauthorized"); } @@ -152,6 +159,7 @@ public async Task Send(Uri uri, HttpMethod httpM _logger?.LogWarning("Response is not json, but {content}", await result.Content.ReadAsStringAsync(cancellationToken)); + return default; } } diff --git a/src/Sinch/SinchApiException.cs b/src/Sinch/SinchApiException.cs index 05429ec8..5bbaa7d9 100644 --- a/src/Sinch/SinchApiException.cs +++ b/src/Sinch/SinchApiException.cs @@ -11,22 +11,22 @@ namespace Sinch /// public sealed class SinchApiException : HttpRequestException { - private SinchApiException(string message, Exception inner, HttpStatusCode statusCode) : base(message, inner, + private SinchApiException(string? message, Exception? inner, HttpStatusCode? statusCode) : base(message, inner, statusCode) { Details = new List(); } - internal SinchApiException(HttpStatusCode statusCode, string message, Exception inner, - ApiErrorResponse authApiError) - : this($"{message}:{authApiError.Error?.Message ?? authApiError.Text}", inner, statusCode) + internal SinchApiException(HttpStatusCode statusCode, string? message, Exception? inner, + ApiErrorResponse? authApiError) + : this($"{message}:{authApiError?.Error?.Message ?? authApiError?.Text}", inner, statusCode) { // https://developers.sinch.com/docs/sms/api-reference/status-codes/#4xx---user-errors // there can be nested error object or simple { text: "", code: "code" } not nested object with api errors // nested object takes precedence in fields population - var details = authApiError.Error; - Status = details?.Status ?? authApiError.Code; - DetailedMessage = details?.Message ?? authApiError.Text; + var details = authApiError?.Error; + Status = details?.Status ?? authApiError?.Code; + DetailedMessage = details?.Message ?? authApiError?.Text; Details = details?.Details ?? new List(); } From cd22935fd835f20db01e4bffdeb6474ba20f3389 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 11 Apr 2024 12:15:19 +0200 Subject: [PATCH 13/19] refactor: utils refs? --- src/Sinch/Core/Utils.cs | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/src/Sinch/Core/Utils.cs b/src/Sinch/Core/Utils.cs index b6bc74ec..75cc02ce 100644 --- a/src/Sinch/Core/Utils.cs +++ b/src/Sinch/Core/Utils.cs @@ -4,8 +4,6 @@ using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Text.Json; -using System.Text.Json.Serialization; namespace Sinch.Core { @@ -35,7 +33,7 @@ private static string GetEnumString(Type type, object value) if (enumMemberAttribute == null) { - return value.ToString(); + return value.ToString() ?? throw new InvalidOperationException("value.ToString() is null"); } return enumMemberAttribute.Value!; @@ -98,7 +96,7 @@ public static string ToSnakeCaseQueryString(T obj) where T : class if (typeof(IEnumerable).IsAssignableFrom(propType) && propType != typeof(string)) { - list.AddRange(ParamsFromObject(propName, propVal as IEnumerable)); + list.AddRange(ParamsFromObject(propName, (propVal as IEnumerable)!)); } else { @@ -130,28 +128,15 @@ private static string ToQueryParamString(object o) if (typeof(bool).IsAssignableFrom(type) || typeof(bool?).IsAssignableFrom(type)) { - return o.ToString()?.ToLowerInvariant(); + return o.ToString()?.ToLowerInvariant()!; } if (typeof(EnumRecord).IsAssignableFrom(type)) { - return type.GetProperty("Value", typeof(string))?.GetValue(o) as string; + return (type.GetProperty("Value", typeof(string))?.GetValue(o) as string)!; } - return o.ToString(); - } - } - - internal class SinchEnumConverter : JsonConverter where T : Enum - { - public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - return Utils.ParseEnum(reader.GetString()); - } - - public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) - { - writer.WriteStringValue(Utils.GetEnumString(value)); + return o.ToString()!; } } } From 2f43b206c3b8603697940780b9701061645ba38c Mon Sep 17 00:00:00 2001 From: Dovchik Date: Wed, 17 Apr 2024 16:21:18 +0200 Subject: [PATCH 14/19] fix: don't throw nullref when T is null in JsonInterfaceConverter.cs --- src/Sinch/Core/JsonInterfaceConverter.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Sinch/Core/JsonInterfaceConverter.cs b/src/Sinch/Core/JsonInterfaceConverter.cs index 3334c7be..fc818e5a 100644 --- a/src/Sinch/Core/JsonInterfaceConverter.cs +++ b/src/Sinch/Core/JsonInterfaceConverter.cs @@ -37,12 +37,7 @@ public class InterfaceConverter : JsonConverter where T : class public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { - if (value is null) - { - throw new NullReferenceException("value is null"); - } - - var type = value.GetType(); + var type = typeof(T); JsonSerializer.Serialize(writer, value, type, options); } } From 3dcd2868bf0428fc8581c39d26741d50c5eec05b Mon Sep 17 00:00:00 2001 From: Dovchik Date: Wed, 17 Apr 2024 16:30:07 +0200 Subject: [PATCH 15/19] refactor: use nullable logger factory --- src/Sinch/Core/Http.cs | 4 ++-- src/Sinch/SinchClient.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Sinch/Core/Http.cs b/src/Sinch/Core/Http.cs index dc96209a..deb1302a 100644 --- a/src/Sinch/Core/Http.cs +++ b/src/Sinch/Core/Http.cs @@ -52,12 +52,12 @@ internal class Http : IHttp { private readonly HttpClient _httpClient; private readonly JsonSerializerOptions _jsonSerializerOptions; - private readonly ILoggerAdapter _logger; + private readonly ILoggerAdapter? _logger; private readonly ISinchAuth _auth; private readonly string _userAgentHeaderValue; - public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter logger, + public Http(ISinchAuth auth, HttpClient httpClient, ILoggerAdapter? logger, JsonNamingPolicy jsonNamingPolicy) { _logger = logger; diff --git a/src/Sinch/SinchClient.cs b/src/Sinch/SinchClient.cs index fd4e84fe..ed5e2951 100644 --- a/src/Sinch/SinchClient.cs +++ b/src/Sinch/SinchClient.cs @@ -138,7 +138,7 @@ public class SinchClient : ISinchClient private readonly string? _keySecret; private readonly string? _projectId; - private readonly LoggerFactory _loggerFactory; + private readonly LoggerFactory? _loggerFactory; private readonly ISinchNumbers _numbers; From 883f97670143a9c65015548211dea435158220a3 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Wed, 17 Apr 2024 16:40:57 +0200 Subject: [PATCH 16/19] chore: add comment for OAuth why suppress nulref --- src/Sinch/SinchClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sinch/SinchClient.cs b/src/Sinch/SinchClient.cs index ed5e2951..57f4f920 100644 --- a/src/Sinch/SinchClient.cs +++ b/src/Sinch/SinchClient.cs @@ -179,6 +179,7 @@ public SinchClient(string? projectId, string? keyId, string? keySecret, _apiUrlOverrides = optionsObj.ApiUrlOverrides; ISinchAuth auth = + // exception is throw when trying to get OAuth or Oauth dependant clients if credentials are missing new OAuth(_keyId!, _keySecret!, _httpClient, _loggerFactory?.Create(), new Uri(_apiUrlOverrides?.AuthUrl ?? AuthApiUrl)); _auth = auth; From d873027c83662f12c90dd4d88634e35203702d4f Mon Sep 17 00:00:00 2001 From: Dovchik Date: Wed, 17 Apr 2024 17:30:35 +0200 Subject: [PATCH 17/19] fix: interface converter use object if value is null --- src/Sinch/Core/JsonInterfaceConverter.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Sinch/Core/JsonInterfaceConverter.cs b/src/Sinch/Core/JsonInterfaceConverter.cs index fc818e5a..47d75350 100644 --- a/src/Sinch/Core/JsonInterfaceConverter.cs +++ b/src/Sinch/Core/JsonInterfaceConverter.cs @@ -37,7 +37,15 @@ public class InterfaceConverter : JsonConverter where T : class public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOptions options) { - var type = typeof(T); + // just serialize as usual null object + if (value is null) + { + JsonSerializer.Serialize(writer, value, typeof(object), options); + return; + } + + // need getType to get an actual instance and not typeof which is always the interface itself + var type = value.GetType(); JsonSerializer.Serialize(writer, value, type, options); } } From 5d5492ce37e90bc46d9e9b1a6b794817f059d54a Mon Sep 17 00:00:00 2001 From: Dovchik Date: Wed, 17 Apr 2024 18:01:05 +0200 Subject: [PATCH 18/19] chore: fix formatting --- src/Sinch/Core/JsonInterfaceConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sinch/Core/JsonInterfaceConverter.cs b/src/Sinch/Core/JsonInterfaceConverter.cs index 47d75350..df9c2953 100644 --- a/src/Sinch/Core/JsonInterfaceConverter.cs +++ b/src/Sinch/Core/JsonInterfaceConverter.cs @@ -43,7 +43,7 @@ public override void Write(Utf8JsonWriter writer, T? value, JsonSerializerOption JsonSerializer.Serialize(writer, value, typeof(object), options); return; } - + // need getType to get an actual instance and not typeof which is always the interface itself var type = value.GetType(); JsonSerializer.Serialize(writer, value, type, options); From 12652e80026652793bea7ec56876494f9ee21473 Mon Sep 17 00:00:00 2001 From: Dovchik Date: Thu, 18 Apr 2024 12:38:51 +0200 Subject: [PATCH 19/19] feat: add tests for JsonInterfaceConverter --- .../Core/JsonInterfaceConverterTests.cs | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 tests/Sinch.Tests/Core/JsonInterfaceConverterTests.cs diff --git a/tests/Sinch.Tests/Core/JsonInterfaceConverterTests.cs b/tests/Sinch.Tests/Core/JsonInterfaceConverterTests.cs new file mode 100644 index 00000000..aa8ca051 --- /dev/null +++ b/tests/Sinch.Tests/Core/JsonInterfaceConverterTests.cs @@ -0,0 +1,75 @@ +using FluentAssertions; +using Sinch.Core; +using Xunit; +using JsonSerializer = System.Text.Json.JsonSerializer; + +namespace Sinch.Tests.Core +{ + public class JsonInterfaceConverterTests + { + [JsonInterfaceConverter(typeof(InterfaceConverter))] + private interface IPerson + { + } + + private class Student : IPerson + { + public string Name { get; set; } + } + + private class Teacher : IPerson + { + public int Age { get; set; } + } + + [Fact] + public void WriteIPersonStudent() + { + IPerson person = new Student() + { + Name = "Adam" + }; + var json = JsonSerializer.Serialize(person); + json.Should().BeEquivalentTo("{\"Name\":\"Adam\"}"); + } + + [Fact] + public void WriteIPersonTeacher() + { + IPerson person = new Teacher() + { + Age = 42 + }; + var json = JsonSerializer.Serialize(person); + json.Should().BeEquivalentTo("{\"Age\":42}"); + } + + [Fact] + public void WriteIPersonNull() + { + var json = JsonSerializer.Serialize((IPerson)null); + json.Should().BeEquivalentTo("null"); + } + + [Fact] + public void ReadIPersonStudent() + { + var person = JsonSerializer.Deserialize("{\"Name\":\"Adam\"}"); + person.Should().BeOfType().Which.Name.Should().Be("Adam"); + } + + [Fact] + public void ReadIPersonTeacher() + { + var person = JsonSerializer.Deserialize("{\"Age\":42}"); + person.Should().BeOfType().Which.Age.Should().Be(42); + } + + [Fact] + public void ReadIPersonNull() + { + var person = JsonSerializer.Deserialize("null"); + person.Should().BeNull(); + } + } +}