global: add null safety checks

This commit is contained in:
Abdullah Atta
2025-10-14 21:15:51 +05:00
parent be432dfd24
commit 6e35edb715
109 changed files with 452 additions and 590 deletions
@@ -43,7 +43,7 @@ namespace Notesnook.API.Authorization
PathString path = context.Resource is DefaultHttpContext httpContext ? httpContext.Request.Path : null;
var result = this.IsAuthorized(context.User, path);
if (result.Succeeded) context.Succeed(requirement);
else if (result.AuthorizationFailure.FailureReasons.Any())
else if (result.AuthorizationFailure?.FailureReasons.Any() == true)
context.Fail(result.AuthorizationFailure.FailureReasons.First());
else context.Fail();
@@ -63,11 +63,11 @@ namespace Notesnook.API.Authorization
return PolicyAuthorizationResult.Forbid(AuthorizationFailure.Failed(reason));
}
var hasSyncScope = User.HasClaim("scope", "notesnook.sync");
var isInAudience = User.HasClaim("aud", "notesnook");
var hasRole = User.HasClaim("role", "notesnook");
var hasSyncScope = User?.HasClaim("scope", "notesnook.sync") ?? false;
var isInAudience = User?.HasClaim("aud", "notesnook") ?? false;
var hasRole = User?.HasClaim("role", "notesnook") ?? false;
var isEmailVerified = User.HasClaim("verified", "true");
var isEmailVerified = User?.HasClaim("verified", "true") ?? false;
if (!isEmailVerified)
{
@@ -57,7 +57,7 @@ namespace Notesnook.API.Controllers
if (item.Type != "callToActions") continue;
foreach (var action in item.Actions)
{
if (action.Type != "link") continue;
if (action.Type != "link" || action.Data == null) continue;
action.Data = action.Data.Replace("{{UserId}}", userId ?? "0");
}
+5 -5
View File
@@ -47,7 +47,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> GetApiKeysAsync()
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
var apiKeys = await inboxApiKeysRepository.FindAsync(t => t.UserId == userId);
@@ -64,7 +64,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> CreateApiKeyAsync([FromBody] InboxApiKey request)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
if (string.IsNullOrWhiteSpace(request.Name))
@@ -104,7 +104,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = "Notesnook")]
public async Task<IActionResult> DeleteApiKeyAsync(string apiKey)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
if (string.IsNullOrWhiteSpace(apiKey))
@@ -126,7 +126,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> GetPublicKeyAsync()
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
var userSetting = await userSettingsRepository.FindOneAsync(u => u.UserId == userId);
@@ -147,7 +147,7 @@ namespace Notesnook.API.Controllers
[Authorize(Policy = InboxApiKeyAuthenticationDefaults.AuthenticationScheme)]
public async Task<IActionResult> CreateInboxItemAsync([FromBody] InboxSyncItem request)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
if (request.Key.Algorithm != Algorithms.XSAL_X25519_7)
@@ -98,9 +98,8 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
var jti = this.User.FindFirstValue("jti");
if (userId == null) return Unauthorized();
var existingMonograph = await FindMonographAsync(userId, monograph);
if (existingMonograph != null && !existingMonograph.Deleted) return await UpdateAsync(deviceId, monograph);
@@ -144,9 +143,8 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
var jti = this.User.FindFirstValue("jti");
if (userId == null) return Unauthorized();
var existingMonograph = await FindMonographAsync(userId, monograph);
if (existingMonograph == null || existingMonograph.Deleted)
@@ -193,8 +191,7 @@ namespace Notesnook.API.Controllers
[HttpGet]
public async Task<IActionResult> GetUserMonographsAsync()
{
var userId = this.User.FindFirstValue("sub");
if (userId == null) return Unauthorized();
var userId = this.User.GetUserId();
var userMonographs = (await monographs.Collection.FindAsync(
Builders<Monograph>.Filter.And(
@@ -257,8 +254,7 @@ namespace Notesnook.API.Controllers
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteAsync([FromQuery] string? deviceId, [FromRoute] string id)
{
var userId = this.User.FindFirstValue("sub");
if (userId is null) return Unauthorized();
var userId = this.User.GetUserId();
var monograph = await FindMonographAsync(id);
if (monograph == null || monograph.Deleted)
@@ -310,12 +306,13 @@ namespace Notesnook.API.Controllers
});
}
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string content)
private async Task<string> CleanupContentAsync(ClaimsPrincipal user, string? content)
{
if (string.IsNullOrEmpty(content)) return string.Empty;
if (Constants.IS_SELF_HOSTED) return content;
try
{
var json = JsonSerializer.Deserialize<MonographContent>(content);
var json = JsonSerializer.Deserialize<MonographContent>(content) ?? throw new Exception("Invalid monograph content.");
var html = json.Data;
if (user.IsUserSubscribed())
@@ -44,7 +44,7 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub") ?? throw new Exception("User not found.");
var userId = this.User.GetUserId();
new SyncDeviceService(new SyncDevice(userId, deviceId)).RegisterDevice();
return Ok();
}
@@ -61,7 +61,7 @@ namespace Notesnook.API.Controllers
{
try
{
var userId = this.User.FindFirstValue("sub") ?? throw new Exception("User not found.");
var userId = this.User.GetUserId();
new SyncDeviceService(new SyncDevice(userId, deviceId)).UnregisterDevice();
return Ok();
}
+4 -4
View File
@@ -55,7 +55,7 @@ namespace Notesnook.API.Controllers
[HttpGet]
public async Task<IActionResult> GetUser()
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
UserResponse response = await UserService.GetUserAsync(userId);
@@ -72,7 +72,7 @@ namespace Notesnook.API.Controllers
[HttpPatch]
public async Task<IActionResult> UpdateUser([FromBody] UserKeys keys)
{
var userId = User.FindFirstValue("sub");
var userId = User.GetUserId();
try
{
await UserService.SetUserKeysAsync(userId, keys);
@@ -88,7 +88,7 @@ namespace Notesnook.API.Controllers
[HttpPost("reset")]
public async Task<IActionResult> Reset([FromForm] bool removeAttachments)
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
if (await UserService.ResetUserAsync(userId, removeAttachments))
return Ok();
@@ -99,7 +99,7 @@ namespace Notesnook.API.Controllers
[RequestTimeout(5 * 60 * 1000)]
public async Task<IActionResult> Delete([FromForm] DeleteAccountForm form)
{
var userId = this.User.FindFirstValue("sub");
var userId = this.User.GetUserId();
var jti = User.FindFirstValue("jti");
try
{
@@ -10,5 +10,8 @@ namespace System.Security.Claims
private readonly static string[] SUBSCRIBED_CLAIMS = ["believer", "education", "essential", "pro", "legacy_pro"];
public static bool IsUserSubscribed(this ClaimsPrincipal user)
=> user.Claims.Any((c) => c.Type == "notesnook:status" && SUBSCRIBED_CLAIMS.Contains(c.Value));
public static string GetUserId(this ClaimsPrincipal user)
=> user.FindFirstValue("sub") ?? throw new Exception("User ID not found in claims.");
}
}
+2 -1
View File
@@ -7,7 +7,7 @@ namespace Notesnook.API.Jobs
{
public class DeviceCleanupJob : IJob
{
public async Task Execute(IJobExecutionContext context)
public Task Execute(IJobExecutionContext context)
{
ParallelOptions parallelOptions = new()
{
@@ -59,6 +59,7 @@ namespace Notesnook.API.Jobs
}
}
});
return Task.CompletedTask;
}
}
}
+21 -21
View File
@@ -40,7 +40,7 @@ namespace Notesnook.API.Models
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("timestamp")]
[BsonElement("timestamp")]
@@ -48,7 +48,7 @@ namespace Notesnook.API.Models
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
public required string[] Platforms { get; set; }
[JsonPropertyName("isActive")]
[BsonElement("isActive")]
@@ -56,7 +56,7 @@ namespace Notesnook.API.Models
[JsonPropertyName("userTypes")]
[BsonElement("userTypes")]
public string[] UserTypes { get; set; }
public required string[] UserTypes { get; set; }
[JsonPropertyName("appVersion")]
[BsonElement("appVersion")]
@@ -64,63 +64,63 @@ namespace Notesnook.API.Models
[JsonPropertyName("body")]
[BsonElement("body")]
public BodyComponent[] Body { get; set; }
public required BodyComponent[] Body { get; set; }
[JsonIgnore]
[BsonElement("userIds")]
public string[] UserIds { get; set; }
public string[]? UserIds { get; set; }
[Obsolete]
[JsonPropertyName("title")]
[DataMember(Name = "title")]
[BsonElement("title")]
public string Title { get; set; }
public string? Title { get; set; }
[Obsolete]
[JsonPropertyName("description")]
[BsonElement("description")]
public string Description { get; set; }
public string? Description { get; set; }
[Obsolete]
[JsonPropertyName("callToActions")]
[BsonElement("callToActions")]
public CallToAction[] CallToActions { get; set; }
public CallToAction[]? CallToActions { get; set; }
}
public class BodyComponent
{
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
public string[]? Platforms { get; set; }
[JsonPropertyName("style")]
[BsonElement("style")]
public Style Style { get; set; }
public Style? Style { get; set; }
[JsonPropertyName("src")]
[BsonElement("src")]
public string Src { get; set; }
public string? Src { get; set; }
[JsonPropertyName("text")]
[BsonElement("text")]
public string Text { get; set; }
public string? Text { get; set; }
[JsonPropertyName("value")]
[BsonElement("value")]
public string Value { get; set; }
public string? Value { get; set; }
[JsonPropertyName("items")]
[BsonElement("items")]
public BodyComponent[] Items { get; set; }
public BodyComponent[]? Items { get; set; }
[JsonPropertyName("actions")]
[BsonElement("actions")]
public CallToAction[] Actions { get; set; }
public required CallToAction[] Actions { get; set; }
}
public class Style
@@ -135,25 +135,25 @@ namespace Notesnook.API.Models
[JsonPropertyName("textAlign")]
[BsonElement("textAlign")]
public string TextAlign { get; set; }
public string? TextAlign { get; set; }
}
public class CallToAction
{
[JsonPropertyName("type")]
[BsonElement("type")]
public string Type { get; set; }
public required string Type { get; set; }
[JsonPropertyName("platforms")]
[BsonElement("platforms")]
public string[] Platforms { get; set; }
public string[]? Platforms { get; set; }
[JsonPropertyName("data")]
[BsonElement("data")]
public string Data { get; set; }
public string? Data { get; set; }
[JsonPropertyName("title")]
[BsonElement("title")]
public string Title { get; set; }
public string? Title { get; set; }
}
}
@@ -5,9 +5,9 @@ namespace Notesnook.API.Models;
public class CompleteMultipartUploadRequestWrapper
{
public string Key { get; set; }
public List<PartETagWrapper> PartETags { get; set; }
public string UploadId { get; set; }
public required string Key { get; set; }
public required List<PartETagWrapper> PartETags { get; set; }
public required string UploadId { get; set; }
public CompleteMultipartUploadRequest ToRequest()
{
+1 -1
View File
@@ -5,7 +5,7 @@ namespace Notesnook.API.Models
public class DeleteAccountForm
{
[Required]
public string Password
public required string Password
{
get; set;
}
+5 -11
View File
@@ -26,25 +26,19 @@ using System.Text.Json.Serialization;
namespace Notesnook.API.Models
{
[MessagePack.MessagePackObject]
public class EncryptedData : IEncrypted
public class EncryptedData
{
[MessagePack.Key("iv")]
[JsonPropertyName("iv")]
[BsonElement("iv")]
[DataMember(Name = "iv")]
public string IV
{
get; set;
}
public required string IV { get; set; }
[MessagePack.Key("cipher")]
[JsonPropertyName("cipher")]
[BsonElement("cipher")]
[DataMember(Name = "cipher")]
public string Cipher
{
get; set;
}
public required string Cipher { get; set; }
[MessagePack.Key("length")]
[JsonPropertyName("length")]
@@ -56,9 +50,9 @@ namespace Notesnook.API.Models
[JsonPropertyName("salt")]
[BsonElement("salt")]
[DataMember(Name = "salt")]
public string Salt { get; set; }
public required string Salt { get; set; }
public override bool Equals(object obj)
public override bool Equals(object? obj)
{
if (obj is EncryptedData encryptedData)
{
+4 -4
View File
@@ -37,16 +37,16 @@ namespace Notesnook.API.Models
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id { get; set; }
public string Id { get; set; } = string.Empty;
[JsonPropertyName("userId")]
public string UserId { get; set; }
public required string UserId { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
public required string Name { get; set; }
[JsonPropertyName("key")]
public string Key { get; set; }
public string Key { get; set; } = string.Empty;
[JsonPropertyName("dateCreated")]
public long DateCreated { get; set; }
+4 -16
View File
@@ -31,19 +31,13 @@ namespace Notesnook.API.Models
[JsonPropertyName("key")]
[MessagePack.Key("key")]
[Required]
public EncryptedKey Key
{
get; set;
}
public required EncryptedKey Key { get; set; }
[DataMember(Name = "salt")]
[JsonPropertyName("salt")]
[MessagePack.Key("salt")]
[Required]
public string Salt
{
get; set;
}
public required string Salt { get; set; }
}
[MessagePack.MessagePackObject]
@@ -53,19 +47,13 @@ namespace Notesnook.API.Models
[JsonPropertyName("alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
}
public required string Algorithm { get; set; }
[DataMember(Name = "cipher")]
[JsonPropertyName("cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
get; set;
}
public required string Cipher { get; set; }
[JsonPropertyName("length")]
[DataMember(Name = "length")]
+5 -17
View File
@@ -29,15 +29,9 @@ namespace Notesnook.API.Models
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
public string Id
{
get; set;
}
public required string Id { get; set; }
public string ItemId
{
get; set;
}
public required string ItemId { get; set; }
}
public class Monograph
@@ -50,23 +44,17 @@ namespace Notesnook.API.Models
[DataMember(Name = "id")]
[JsonPropertyName("id")]
[MessagePack.Key("id")]
public string ItemId
{
get; set;
}
public string? ItemId { get; set; }
[BsonId]
[BsonIgnoreIfDefault]
[BsonRepresentation(BsonType.ObjectId)]
[JsonIgnore]
[MessagePack.IgnoreMember]
public string Id
{
get; set;
}
public string Id { get; set; } = string.Empty;
[JsonPropertyName("title")]
public string Title { get; set; }
public string? Title { get; set; }
[JsonPropertyName("userId")]
public string? UserId { get; set; }
+2 -2
View File
@@ -28,8 +28,8 @@ namespace Notesnook.API.Models
public class MonographContent
{
[JsonPropertyName("data")]
public string Data { get; set; }
public required string Data { get; set; }
[JsonPropertyName("type")]
public string Type { get; set; }
public required string Type { get; set; }
}
}
+1 -1
View File
@@ -35,7 +35,7 @@ namespace Notesnook.API.Models
}
[JsonPropertyName("title")]
public required string Title { get; set; }
public string? Title { get; set; }
[JsonPropertyName("selfDestruct")]
public bool SelfDestruct { get; set; }
+4 -2
View File
@@ -17,11 +17,13 @@ You should have received a copy of the Affero GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System;
namespace Notesnook.API.Models
{
public class MultipartUploadMeta
{
public string UploadId { get; set; }
public string[] Parts { get; set; }
public string UploadId { get; set; } = string.Empty;
public string[] Parts { get; set; } = Array.Empty<string>();
}
}
+1 -1
View File
@@ -3,5 +3,5 @@
public class PartETagWrapper
{
public int PartNumber { get; set; }
public string ETag { get; set; }
public string ETag { get; set; } = string.Empty;
}
@@ -6,9 +6,9 @@ namespace Notesnook.API.Models.Responses
public class SignupResponse : Response
{
[JsonPropertyName("userId")]
public string UserId { get; set; }
public string? UserId { get; set; }
[JsonPropertyName("errors")]
public string[] Errors { get; set; }
public string[]? Errors { get; set; }
}
}
+4 -4
View File
@@ -21,9 +21,9 @@ namespace Notesnook.API.Models
{
public class S3Options
{
public string ServiceUrl { get; set; }
public string Region { get; set; }
public string AccessKeyId { get; set; }
public string SecretAccessKey { get; set; }
public string ServiceUrl { get; set; } = string.Empty;
public string Region { get; set; } = string.Empty;
public string AccessKeyId { get; set; } = string.Empty;
public string SecretAccessKey { get; set; } = string.Empty;
}
}
+3 -12
View File
@@ -53,20 +53,14 @@ namespace Notesnook.API.Models
[DataMember(Name = "iv")]
[MessagePack.Key("iv")]
[Required]
public string IV
{
get; set;
}
public string IV { get; set; } = string.Empty;
[JsonPropertyName("cipher")]
[DataMember(Name = "cipher")]
[MessagePack.Key("cipher")]
[Required]
public string Cipher
{
get; set;
}
public string Cipher { get; set; } = string.Empty;
[DataMember(Name = "id")]
[JsonPropertyName("id")]
@@ -108,10 +102,7 @@ namespace Notesnook.API.Models
[DataMember(Name = "alg")]
[MessagePack.Key("alg")]
[Required]
public string Algorithm
{
get; set;
}
public string Algorithm { get; set; } = string.Empty;
}
public class SyncItemBsonSerializer : SerializerBase<SyncItem>
+6 -6
View File
@@ -29,23 +29,23 @@ namespace Notesnook.API.Models
public long UpdatedAt { get; set; }
}
public class UserSettings : IUserSettings
public class UserSettings
{
public UserSettings()
{
this.Id = ObjectId.GenerateNewId().ToString();
this.Id = ObjectId.GenerateNewId();
}
public string UserId { get; set; }
public required string UserId { get; set; }
public long LastSynced { get; set; }
public string Salt { get; set; }
public required string Salt { get; set; }
public EncryptedData? VaultKey { get; set; }
public EncryptedData? AttachmentsKey { get; set; }
public EncryptedData? MonographPasswordsKey { get; set; }
public InboxKeys? InboxKeys { get; set; }
public Limit StorageLimit { get; set; }
public Limit? StorageLimit { get; set; }
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
public ObjectId Id { get; set; }
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ namespace Notesnook.API
{
options.Limits.MaxRequestBodySize = long.MaxValue;
options.ListenAnyIP(Servers.NotesnookAPI.Port);
if (Servers.NotesnookAPI.IsSecure)
if (Servers.NotesnookAPI.IsSecure && Servers.NotesnookAPI.SSLCertificate != null)
{
options.ListenAnyIP(443, listenerOptions =>
{
+3 -3
View File
@@ -52,7 +52,7 @@ namespace Notesnook.API.Services
public async Task CreateUserAsync()
{
SignupResponse response = await httpClient.ForwardAsync<SignupResponse>(this.HttpContextAccessor, $"{Servers.IdentityServer}/signup", HttpMethod.Post);
if (!response.Success || (response.Errors != null && response.Errors.Length > 0))
if (!response.Success || (response.Errors != null && response.Errors.Length > 0) || response.UserId == null)
{
logger.LogError("Failed to sign up user: {Response}", JsonSerializer.Serialize(response));
if (response.Errors != null && response.Errors.Length > 0)
@@ -216,7 +216,7 @@ namespace Notesnook.API.Services
await S3Service.DeleteDirectoryAsync(userId);
}
public async Task DeleteUserAsync(string userId, string jti, string password)
public async Task DeleteUserAsync(string userId, string? jti, string password)
{
logger.LogInformation("Deleting user account: {UserId}", userId);
@@ -227,7 +227,7 @@ namespace Notesnook.API.Services
await WampServers.MessengerServer.PublishMessageAsync(MessengerServerTopics.SendSSETopic, new SendSSEMessage
{
SendToAll = false,
SendToAll = jti == null,
OriginTokenId = jti,
UserId = userId,
Message = new Message
+5 -10
View File
@@ -119,8 +119,8 @@ namespace Notesnook.API
policy.RequireAuthenticatedUser();
});
options.DefaultPolicy = options.GetPolicy("Notesnook");
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>(); ;
options.DefaultPolicy = options.GetPolicy("Notesnook") ?? throw new Exception("Notesnook policy not found");
}).AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationResultTransformer>();
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddOAuth2Introspection("introspection", options =>
@@ -138,13 +138,13 @@ namespace Notesnook.API
options.Events.OnTokenValidated = (context) =>
{
if (long.TryParse(context.Principal.FindFirst("exp")?.Value, out long expiryTime))
if (long.TryParse(context.Principal?.FindFirst("exp")?.Value, out long expiryTime))
{
context.Properties.ExpiresUtc = DateTimeOffset.FromUnixTimeSeconds(expiryTime);
}
context.Properties.AllowRefresh = true;
context.Properties.IsPersistent = true;
context.HttpContext.User = context.Principal;
context.HttpContext.User = context.Principal ?? throw new Exception("No principal found in token.");
return Task.CompletedTask;
};
options.CacheKeyGenerator = (options, token) => (token + ":" + "reference_token").Sha256();
@@ -289,11 +289,6 @@ namespace Notesnook.API
{
endpoints.MapControllers();
endpoints.MapHealthChecks("/health");
endpoints.MapHub<SyncHub>("/hubs/sync", options =>
{
options.CloseOnAuthenticationExpiration = false;
options.Transports = HttpTransportType.WebSockets;
});
endpoints.MapHub<SyncV2Hub>("/hubs/sync/v2", options =>
{
options.CloseOnAuthenticationExpiration = false;
@@ -307,7 +302,7 @@ namespace Notesnook.API
{
public static IServiceCollection AddMongoCollection(this IServiceCollection services, string collectionName, string database = "notesnook")
{
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetService<MongoDB.Driver.IMongoClient>(), database, collectionName));
services.AddKeyedSingleton(collectionName, (provider, key) => MongoDbContext.GetMongoCollection<SyncItem>(provider.GetRequiredService<MongoDB.Driver.IMongoClient>(), database, collectionName));
return services;
}
}