Initial commit carried over from private repo. This is V2.
This commit is contained in:
@ -0,0 +1,9 @@
|
||||
namespace MessengerApi.Configuration.Enums
|
||||
{
|
||||
public enum HousekeepingMessageStates
|
||||
{
|
||||
None,
|
||||
Delivered,
|
||||
Acknowledged
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace MessengerApi.Configuration.Enums
|
||||
{
|
||||
public enum LoggingVerbosity
|
||||
{
|
||||
Normal,
|
||||
Debug,
|
||||
Trace
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
namespace MessengerApi.Configuration.Enums
|
||||
{
|
||||
public enum PersistenceTypes
|
||||
{
|
||||
Sql,
|
||||
PostgreSql
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,98 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
using MessengerApi.Configuration.Model.Persistence.Base;
|
||||
using MessengerApi.Configuration.Parsers;
|
||||
using MessengerApi.Configuration.Sources.Environment;
|
||||
using Env = MessengerApi.Configuration.Constants.EnvironmentVariables;
|
||||
|
||||
namespace MessengerApi.Configuration.Model
|
||||
{
|
||||
public class MessengerConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// CORS origins.
|
||||
/// </summary>
|
||||
public string[] Origins { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// List of proxies that are trusted to provide forwarding headers.
|
||||
/// </summary>
|
||||
public string[] Proxies { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Persistence layer configs (database).
|
||||
/// </summary>
|
||||
public PersistenceConfiguration PersistenceConfiguration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Limits rate of user calls to not DoS the service.
|
||||
/// </summary>
|
||||
public int RateLimitPerMinute { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Message lifetime unless set differently in message body.
|
||||
/// </summary>
|
||||
public int DefaultMessageLifetimeInMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, messages are periodically wiped to free up space.
|
||||
/// </summary>
|
||||
public bool HousekeepingEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Messages older than value set will be deleted regardless of their delivery state.
|
||||
/// </summary>
|
||||
public int HousekeepingMessageAgeInMinutes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines level of log messages displayed.
|
||||
/// </summary>
|
||||
public LoggingVerbosity Verbosity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In addition to <see cref="HousekeepingMessageAgeInMinutes"/> messages of certain state can also be deleted, increasing storage efficiency.
|
||||
/// </summary>
|
||||
public HousekeepingMessageStates HousekeepingMessageState { get; set; }
|
||||
|
||||
public MessengerConfiguration() { }
|
||||
|
||||
public MessengerConfiguration(string[] origins, PersistenceConfiguration persistenceConfiguration)
|
||||
{
|
||||
if(persistenceConfiguration == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(persistenceConfiguration));
|
||||
}
|
||||
|
||||
this.PersistenceConfiguration = persistenceConfiguration;
|
||||
this.Origins = origins ?? [];
|
||||
this.Proxies = [];
|
||||
this.RateLimitPerMinute = 120;
|
||||
this.DefaultMessageLifetimeInMinutes = 1;
|
||||
this.HousekeepingEnabled = true;
|
||||
this.HousekeepingMessageAgeInMinutes = 120;
|
||||
this.HousekeepingMessageState = HousekeepingMessageStates.None;
|
||||
this.Verbosity = LoggingVerbosity.Normal;
|
||||
}
|
||||
|
||||
public MessengerConfiguration(IEnvironmentConfigurationSource config) : this(
|
||||
CorsParser.Parse(config.GetValue<string>(Env.CORS_ORIGINS)),
|
||||
EnvironmentPersistenceConfigurationParser.Parse(config))
|
||||
{
|
||||
Populate<string>(config, Env.PROXIES, x => this.Proxies = ProxiesParser.Parse(x));
|
||||
Populate<int>(config, Env.QUERY_RATE_PER_MINUTE, x => this.RateLimitPerMinute = x);
|
||||
Populate<int>(config, Env.DEFAULT_MESSAGE_LIFETIME_IN_MINUTES, x => this.DefaultMessageLifetimeInMinutes = x);
|
||||
Populate<bool>(config, Env.HOUSEKEEPING_ENABLED, x => this.HousekeepingEnabled = x);
|
||||
Populate<int>(config, Env.HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES, x => this.HousekeepingMessageAgeInMinutes = x);
|
||||
Populate<string>(config, Env.HOUSEKEEPING_MESSAGE_STATE, x => this.HousekeepingMessageState = HousekeepingMessageStateParser.Parse(x));
|
||||
Populate<string>(config, Env.LOGGING_VERBOSITY, x => this.Verbosity = LoggingVerbosityParser.Parse(x));
|
||||
|
||||
void Populate<T>(IEnvironmentConfigurationSource config, string key, Action<T> set)
|
||||
{
|
||||
if (config.HasKey(key))
|
||||
{
|
||||
var value = config.GetValue<T>(key);
|
||||
set(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
|
||||
namespace MessengerApi.Configuration.Model.Persistence.Base
|
||||
{
|
||||
public abstract class PersistenceConfiguration
|
||||
{
|
||||
public abstract PersistenceTypes PersistenceType { get; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
using MessengerApi.Configuration.Model.Persistence.Base;
|
||||
using MessengerApi.Configuration.Sources.Environment;
|
||||
|
||||
namespace MessengerApi.Configuration.Model.Persistence
|
||||
{
|
||||
public class NpgPersistenceConfiguration : PersistenceConfiguration
|
||||
{
|
||||
public override PersistenceTypes PersistenceType => PersistenceTypes.PostgreSql;
|
||||
|
||||
public string ConnectionString { get; }
|
||||
|
||||
public NpgPersistenceConfiguration(string connectionString)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public NpgPersistenceConfiguration(IEnvironmentConfigurationSource config) : this(config.GetValue<string>(Constants.EnvironmentVariables.NPG_CONNECTIONSTRING)) { }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
using MessengerApi.Configuration.Model.Persistence.Base;
|
||||
using MessengerApi.Configuration.Sources.Environment;
|
||||
|
||||
namespace MessengerApi.Configuration.Model.Persistence
|
||||
{
|
||||
public class SqlPersistenceConfiguration : PersistenceConfiguration
|
||||
{
|
||||
public override PersistenceTypes PersistenceType => PersistenceTypes.Sql;
|
||||
|
||||
public string ConnectionString { get; }
|
||||
|
||||
public SqlPersistenceConfiguration(string connectionString)
|
||||
{
|
||||
ConnectionString = connectionString;
|
||||
}
|
||||
|
||||
public SqlPersistenceConfiguration(IEnvironmentConfigurationSource config) : this(config.GetValue<string>(Constants.EnvironmentVariables.SQL_CONNECTIONSTRING)) { }
|
||||
}
|
||||
}
|
||||
11
code/MessengerApi.Configuration/Parsers/CorsParser.cs
Normal file
11
code/MessengerApi.Configuration/Parsers/CorsParser.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace MessengerApi.Configuration.Parsers
|
||||
{
|
||||
public static class CorsParser
|
||||
{
|
||||
public static string[] Parse(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return [];
|
||||
return value.Trim().Split(",", StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
using MessengerApi.Configuration.Model.Persistence;
|
||||
using MessengerApi.Configuration.Model.Persistence.Base;
|
||||
using MessengerApi.Configuration.Sources.Environment;
|
||||
|
||||
namespace MessengerApi.Configuration.Parsers
|
||||
{
|
||||
public static class EnvironmentPersistenceConfigurationParser
|
||||
{
|
||||
public static PersistenceConfiguration Parse(IEnvironmentConfigurationSource config)
|
||||
{
|
||||
var type = PersistenceTypeParser.Parse(config.GetValue<string>(Constants.EnvironmentVariables.PERSISTENCE_TYPE));
|
||||
|
||||
if(type == Enums.PersistenceTypes.Sql)
|
||||
{
|
||||
return new SqlPersistenceConfiguration(config);
|
||||
}
|
||||
else if(type == Enums.PersistenceTypes.PostgreSql)
|
||||
{
|
||||
return new NpgPersistenceConfiguration(config);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Unrecognized persistence type.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
|
||||
namespace MessengerApi.Configuration.Parsers
|
||||
{
|
||||
public static class HousekeepingMessageStateParser
|
||||
{
|
||||
public static HousekeepingMessageStates Parse(string input)
|
||||
{
|
||||
return (HousekeepingMessageStates)Enum.Parse(typeof(HousekeepingMessageStates), input.Trim(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
|
||||
namespace MessengerApi.Configuration.Parsers
|
||||
{
|
||||
public static class LoggingVerbosityParser
|
||||
{
|
||||
public static LoggingVerbosity Parse(string value)
|
||||
{
|
||||
return (LoggingVerbosity)Enum.Parse(typeof(LoggingVerbosity), value.Trim(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using MessengerApi.Configuration.Enums;
|
||||
|
||||
namespace MessengerApi.Configuration.Parsers
|
||||
{
|
||||
public static class PersistenceTypeParser
|
||||
{
|
||||
public static PersistenceTypes Parse(string value)
|
||||
{
|
||||
return (PersistenceTypes)Enum.Parse(typeof(PersistenceTypes), value, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
code/MessengerApi.Configuration/Parsers/ProxiesParser.cs
Normal file
11
code/MessengerApi.Configuration/Parsers/ProxiesParser.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace MessengerApi.Configuration.Parsers
|
||||
{
|
||||
public static class ProxiesParser
|
||||
{
|
||||
public static string[] Parse(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return [];
|
||||
return value.Trim().Split(",", StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,20 @@
|
||||
namespace MessengerApi.Configuration
|
||||
{
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class EnvironmentVariables
|
||||
{
|
||||
public const string SQL_CONNECTIONSTRING = nameof(SQL_CONNECTIONSTRING);
|
||||
public const string NPG_CONNECTIONSTRING = nameof(NPG_CONNECTIONSTRING);
|
||||
public const string PERSISTENCE_TYPE = nameof(PERSISTENCE_TYPE);
|
||||
public const string CORS_ORIGINS = nameof(CORS_ORIGINS);
|
||||
public const string PROXIES = nameof(PROXIES);
|
||||
public const string QUERY_RATE_PER_MINUTE = nameof(QUERY_RATE_PER_MINUTE);
|
||||
public const string DEFAULT_MESSAGE_LIFETIME_IN_MINUTES = nameof(DEFAULT_MESSAGE_LIFETIME_IN_MINUTES);
|
||||
public const string HOUSEKEEPING_ENABLED = nameof(HOUSEKEEPING_ENABLED);
|
||||
public const string HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES = nameof(HOUSEKEEPING_MESSAGE_AGE_IN_MINUTES);
|
||||
public const string HOUSEKEEPING_MESSAGE_STATE = nameof(HOUSEKEEPING_MESSAGE_STATE);
|
||||
public const string LOGGING_VERBOSITY = nameof(LOGGING_VERBOSITY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
namespace MessengerApi.Configuration.Sources.Environment
|
||||
{
|
||||
public class EnvironmentConfigurationSource : IEnvironmentConfigurationSource
|
||||
{
|
||||
public bool HasKey(string key)
|
||||
{
|
||||
return !string.IsNullOrWhiteSpace(System.Environment.GetEnvironmentVariable(key));
|
||||
}
|
||||
|
||||
public T GetValue<T>(string key)
|
||||
{
|
||||
return (T)Convert.ChangeType(System.Environment.GetEnvironmentVariable(key), typeof(T));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
namespace MessengerApi.Configuration.Sources.Environment
|
||||
{
|
||||
public interface IEnvironmentConfigurationSource : IConfigurationSource
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
namespace MessengerApi.Configuration.Sources
|
||||
{
|
||||
public interface IConfigurationSource
|
||||
{
|
||||
bool HasKey(string key);
|
||||
|
||||
T GetValue<T>(string key);
|
||||
}
|
||||
}
|
||||
22
code/MessengerApi.Contracts.MessageParser/IMessageParser.cs
Normal file
22
code/MessengerApi.Contracts.MessageParser/IMessageParser.cs
Normal file
@ -0,0 +1,22 @@
|
||||
namespace MessengerApi.Contracts.MessageParser
|
||||
{
|
||||
/// <summary>
|
||||
/// A tool that helps converting POCO request/response models into <see cref="InboxMessage"/>, <see cref="OutboxMessage"/> and back.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you implement this, it's gonna be a lot easier for you to translate
|
||||
/// dumb request class into <see cref="OutboxMessage"/>, then convert
|
||||
/// <see cref="InboxMessage"/> back to request at server-side, and do the
|
||||
/// same with the response all the way down to the client.
|
||||
/// </remarks>
|
||||
public interface IMessageParser<TRequest, TResponse>
|
||||
{
|
||||
OutboxMessage GetMessageFromRequest(TRequest request, int targetUserId);
|
||||
|
||||
TRequest GetRequestFromMessage(InboxMessage message);
|
||||
|
||||
OutboxMessage GetMessageFromResponse(TResponse response, string apiKey, int targetUserId, InboxMessage requestOrigin = null);
|
||||
|
||||
TResponse GetResponseFromMessage(InboxMessage message);
|
||||
}
|
||||
}
|
||||
50
code/MessengerApi.Contracts.MessageParser/MessageParser.cs
Normal file
50
code/MessengerApi.Contracts.MessageParser/MessageParser.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace MessengerApi.Contracts.MessageParser
|
||||
{
|
||||
public class MessageParser<TRequest, TResponse> : IMessageParser<TRequest, TResponse>
|
||||
{
|
||||
public OutboxMessage GetMessageFromRequest(
|
||||
TRequest request,
|
||||
int targetUserId)
|
||||
{
|
||||
var message = new OutboxMessage
|
||||
{
|
||||
|
||||
}
|
||||
apikey,
|
||||
targetUserId,
|
||||
typeof(TRequest).Name,
|
||||
JsonConvert.SerializeObject(request));
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public OutboxMessage GetMessageFromResponse(
|
||||
TResponse response,
|
||||
string apiKey,
|
||||
int targetUserId,
|
||||
InboxMessage requestOrigin = null)
|
||||
{
|
||||
var message = new OutboxMessage(
|
||||
apiKey,
|
||||
targetUserId,
|
||||
requestOrigin.PayloadType,
|
||||
JsonConvert.SerializeObject(response));
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
public TRequest GetRequestFromMessage(InboxMessage message)
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<TRequest>(message.Payload);
|
||||
return request;
|
||||
}
|
||||
|
||||
public TResponse GetResponseFromMessage(InboxMessage message)
|
||||
{
|
||||
var request = JsonConvert.DeserializeObject<TResponse>(message.Payload);
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<AssemblyVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</AssemblyVersion>
|
||||
<PackageVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</PackageVersion>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MessengerApi.Contracts" Version="2025.6.28.2202" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
30
code/MessengerApi.Contracts/Client/IMessengerClient.cs
Normal file
30
code/MessengerApi.Contracts/Client/IMessengerClient.cs
Normal file
@ -0,0 +1,30 @@
|
||||
namespace MessengerApi.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Exists for mocking reason. This is implemented by <see cref="MessengerClient"/>.
|
||||
/// </summary>
|
||||
public interface IMessengerClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Receives pending messages from the messenger API.
|
||||
/// </summary>
|
||||
/// <param name="credentials">Credentials to the API.</param>
|
||||
IEnumerable<InboxMessage> GetMessages();
|
||||
|
||||
/// <summary>
|
||||
/// Acknowledges message reception to the server.
|
||||
/// </summary>
|
||||
void AckMessage(InboxMessage message);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a message.
|
||||
/// </summary>
|
||||
/// <param name="credentials">Credentials to the API.</param>
|
||||
void SendMessage(OutboxMessage outboxMessage);
|
||||
|
||||
/// <summary>
|
||||
/// Returns user ids for allowed message recipients.
|
||||
/// </summary>
|
||||
Contact[] GetYellowPages();
|
||||
}
|
||||
}
|
||||
150
code/MessengerApi.Contracts/Client/MessengerClient.cs
Normal file
150
code/MessengerApi.Contracts/Client/MessengerClient.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using portaloggy;
|
||||
using System.Text;
|
||||
using System.Text.Json.Nodes;
|
||||
|
||||
namespace MessengerApi.Contracts
|
||||
{
|
||||
public class MessengerClient : IMessengerClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private DateTime _lastReceivedUtc;
|
||||
private Credentials _credentials;
|
||||
|
||||
public MessengerClient(Credentials credentials, HttpClient httpClient = null, ILogger logger = null)
|
||||
{
|
||||
_credentials = credentials;
|
||||
_httpClient = httpClient ?? new HttpClient();
|
||||
_logger = logger ?? new ConsoleLogger();
|
||||
_lastReceivedUtc = DateTime.MinValue.ToUniversalTime();
|
||||
}
|
||||
|
||||
public IEnumerable<InboxMessage> GetMessages()
|
||||
{
|
||||
var since = Uri.EscapeDataString(this._lastReceivedUtc.ToString("o"));
|
||||
var url = $"{_credentials.ApiUrl}/receive?sinceUtc={since}";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _credentials.ApiKey);
|
||||
|
||||
_logger.Debug($"Sending query to {url} with content {request.ToString()} to obtain messages.");
|
||||
|
||||
var response = _httpClient.Send(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.Error(response.ReasonPhrase);
|
||||
throw new HttpRequestException("Can't receive.", null, response.StatusCode);
|
||||
}
|
||||
|
||||
var responseContent = response.Content
|
||||
.ReadAsStringAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(responseContent))
|
||||
{
|
||||
_logger.Debug($"Received response of {responseContent}.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(responseContent))
|
||||
{
|
||||
return Enumerable.Empty<InboxMessage>().ToArray();
|
||||
}
|
||||
|
||||
var json = JsonNode.Parse(responseContent);
|
||||
var messages = new List<InboxMessage>();
|
||||
|
||||
foreach (var item in json["messages"].AsArray())
|
||||
{
|
||||
if (item["id"].GetValue<int>() == -1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
messages.Add(new InboxMessage
|
||||
{
|
||||
Id = item["id"].GetValue<Guid>(),
|
||||
Payload = item["payload"].ToJsonString(),
|
||||
PayloadId = item["payloadId"].ToJsonString(),
|
||||
PayloadType = item["payloadType"].ToJsonString(),
|
||||
Sender = item["sender"].GetValue<Guid>(),
|
||||
SenderTimestamp = item["senderTimestamp"].GetValue<DateTime>()
|
||||
});
|
||||
}
|
||||
|
||||
_lastReceivedUtc = DateTime.UtcNow.Subtract(TimeSpan.FromSeconds(10));
|
||||
_logger.Debug($"Received {messages.Count} messages and last check timestamp is set to {_lastReceivedUtc.ToString("s")}.");
|
||||
|
||||
return messages.ToArray();
|
||||
}
|
||||
|
||||
public void SendMessage(OutboxMessage outboxMessage)
|
||||
{
|
||||
var body = new JsonObject();
|
||||
|
||||
if(outboxMessage.ToUserId.HasValue)
|
||||
{
|
||||
body.Add("toUserId", JsonValue.Create<Guid>(outboxMessage.ToUserId.Value));
|
||||
}
|
||||
|
||||
if(outboxMessage.Payload != null)
|
||||
{
|
||||
body.Add("payload", JsonValue.Create<string>(outboxMessage.Payload));
|
||||
}
|
||||
|
||||
var content = new StringContent(body.ToString(), Encoding.UTF8, "application/json");
|
||||
|
||||
var url = $"{_credentials.ApiUrl}/send";
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _credentials.ApiKey);
|
||||
|
||||
_logger.Debug($"Sending query to {url} with content {body.ToString()} to obtain messages.");
|
||||
|
||||
var response = _httpClient.Send(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.Error(response.ReasonPhrase);
|
||||
throw new HttpRequestException("Can't send.", null, response.StatusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public Contact[] GetYellowPages()
|
||||
{
|
||||
var url = $"{_credentials.ApiUrl}/yellowpages";
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _credentials.ApiKey);
|
||||
|
||||
_logger.Debug($"Sending query to {url} with content {request.ToString()}.");
|
||||
|
||||
var response = _httpClient.Send(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
_logger.Error(response.ReasonPhrase);
|
||||
throw new HttpRequestException("Can't receive.", null, response.StatusCode);
|
||||
}
|
||||
|
||||
var responseContent = response.Content
|
||||
.ReadAsStringAsync()
|
||||
.GetAwaiter()
|
||||
.GetResult();
|
||||
|
||||
var json = JsonNode.Parse(responseContent);
|
||||
|
||||
var contacts = json["users"].AsArray().Select(x => new Contact
|
||||
{
|
||||
Id = x["id"].GetValue<Guid>(),
|
||||
Name = x["name"].GetValue<string>()
|
||||
}).ToArray();
|
||||
|
||||
return contacts;
|
||||
}
|
||||
|
||||
public void AckMessage(InboxMessage message)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
9
code/MessengerApi.Contracts/Contact.cs
Normal file
9
code/MessengerApi.Contracts/Contact.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace MessengerApi.Contracts
|
||||
{
|
||||
public class Contact
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
15
code/MessengerApi.Contracts/Credentials.cs
Normal file
15
code/MessengerApi.Contracts/Credentials.cs
Normal file
@ -0,0 +1,15 @@
|
||||
namespace MessengerApi.Contracts
|
||||
{
|
||||
public class Credentials
|
||||
{
|
||||
public string ApiKey { get; private set; }
|
||||
|
||||
public string ApiUrl { get; private set; }
|
||||
|
||||
public Credentials(string apiKey, string apiUrl)
|
||||
{
|
||||
ApiKey = apiKey;
|
||||
ApiUrl = apiUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
20
code/MessengerApi.Contracts/Messages/InboxMessage.cs
Normal file
20
code/MessengerApi.Contracts/Messages/InboxMessage.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace MessengerApi.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Message when received is inbox. For server apps, this is request-type of message. For clients, this is a response-type of message.
|
||||
/// </summary>
|
||||
public class InboxMessage
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid Sender { get; set; }
|
||||
|
||||
public DateTime? SenderTimestamp { get; set; }
|
||||
|
||||
public string PayloadId { get; set; }
|
||||
|
||||
public string PayloadType { get; set; }
|
||||
|
||||
public string Payload { get; set; }
|
||||
}
|
||||
}
|
||||
20
code/MessengerApi.Contracts/Messages/OutboxMessage.cs
Normal file
20
code/MessengerApi.Contracts/Messages/OutboxMessage.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace MessengerApi.Contracts
|
||||
{
|
||||
/// <summary>
|
||||
/// Outbox type of message. A server-app will treat this as a response. A client app will treat this as a request.
|
||||
/// </summary>
|
||||
public class OutboxMessage
|
||||
{
|
||||
public Guid? ToUserId { get; set; }
|
||||
|
||||
public string PayloadId { get; set; }
|
||||
|
||||
public string PayloadType { get; set; }
|
||||
|
||||
public string Payload { get; set; }
|
||||
|
||||
public DateTime? PayloadTimestamp { get; set; }
|
||||
|
||||
public int? PayloadLifespanInSeconds { get; set; }
|
||||
}
|
||||
}
|
||||
18
code/MessengerApi.Contracts/MessengerApi.Contracts.csproj
Normal file
18
code/MessengerApi.Contracts/MessengerApi.Contracts.csproj
Normal file
@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageOutputPath>$(OutputPath)</PackageOutputPath>
|
||||
<AssemblyVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</AssemblyVersion>
|
||||
<PackageVersion>$([System.DateTime]::UtcNow.ToString("yyyy.MM.dd.HHmm"))</PackageVersion>
|
||||
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="portaloggy" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
11
code/MessengerApi.Db.Contracts/Entities/IEntity.cs
Normal file
11
code/MessengerApi.Db.Contracts/Entities/IEntity.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace MessengerApi.Db.Contracts.Entities
|
||||
{
|
||||
public interface IEntity
|
||||
{
|
||||
Guid Id { get; }
|
||||
}
|
||||
|
||||
public interface IEntity<T> : IEntity where T : class, IEntity
|
||||
{
|
||||
}
|
||||
}
|
||||
25
code/MessengerApi.Db.Contracts/Entities/Message.cs
Normal file
25
code/MessengerApi.Db.Contracts/Entities/Message.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using MessengerApi.Db.Contracts.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Entities
|
||||
{
|
||||
public class Message : IEntity<Message>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public DateTime CreatedUtc { get; set; }
|
||||
|
||||
public Guid FromId { get; set; }
|
||||
|
||||
public Guid ToId { get; set; }
|
||||
|
||||
public bool IsDelivered { get; set; }
|
||||
|
||||
public bool IsAcknowledged { get; set; }
|
||||
|
||||
public string PayloadType { get; set; }
|
||||
|
||||
public string Payload { get; set; }
|
||||
|
||||
public int? PayloadLifespanInSeconds { get; set; }
|
||||
}
|
||||
}
|
||||
15
code/MessengerApi.Db.Contracts/Entities/User.cs
Normal file
15
code/MessengerApi.Db.Contracts/Entities/User.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using MessengerApi.Db.Contracts.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Entities
|
||||
{
|
||||
public class User : IEntity<User>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid ApiKey { get; set; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public bool IsEnabled { get; set; }
|
||||
}
|
||||
}
|
||||
16
code/MessengerApi.Db.Contracts/Entities/UserRoute.cs
Normal file
16
code/MessengerApi.Db.Contracts/Entities/UserRoute.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using MessengerApi.Db.Contracts.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes allowed message route (who can message whom).
|
||||
/// </summary>
|
||||
public class UserRoute : IEntity<UserRoute>
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public User From { get; set; }
|
||||
|
||||
public User To { get; set; }
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@ -0,0 +1,9 @@
|
||||
using MessengerApi.Db.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Contracts.Repositories
|
||||
{
|
||||
public interface IMessageRepository : IRepository<Message>
|
||||
{
|
||||
IEnumerable<Message> GetPendingMessages(User user);
|
||||
}
|
||||
}
|
||||
15
code/MessengerApi.Db.Contracts/Repositories/IRepository.cs
Normal file
15
code/MessengerApi.Db.Contracts/Repositories/IRepository.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using MessengerApi.Db.Contracts.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Contracts.Repositories
|
||||
{
|
||||
public interface IRepository
|
||||
{
|
||||
}
|
||||
|
||||
public interface IRepository<T> : IRepository where T : class, IEntity<T>
|
||||
{
|
||||
void Add(T entity);
|
||||
|
||||
T GetById(Guid id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using MessengerApi.Db.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Contracts.Repositories
|
||||
{
|
||||
public interface IUserRepository : IRepository<User>
|
||||
{
|
||||
User SingleByApiKeyAndEnabled(Guid id, bool enabled);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
using MessengerApi.Db.Entities;
|
||||
|
||||
namespace MessengerApi.Db.Contracts.Repositories
|
||||
{
|
||||
public interface IUserRouteRepository:IRepository<UserRoute>
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns all routes for given user.
|
||||
/// </summary>
|
||||
IEnumerable<UserRoute> GetAllByUser(User sender);
|
||||
|
||||
/// <summary>
|
||||
/// Returns routes where given user is sender.
|
||||
/// </summary>
|
||||
IEnumerable<UserRoute> GetByFrom(User user);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
using MessengerApi.Db.Npg;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace MessengerApi.Db.Sql.Migrator
|
||||
{
|
||||
public partial class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MessengerNpgDbContext>
|
||||
{
|
||||
public MessengerNpgDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
return new MessengerNpgDbContext(this.ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
16
code/MessengerApi.Db.Npg.Migrator/Program.cs
Normal file
16
code/MessengerApi.Db.Npg.Migrator/Program.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace MessengerApi.Db.Npg.Migrator
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// You can use empty string to build the context when adding a migration - adding a migration does not hit the DB.
|
||||
|
||||
// DesignTimeDbFactory.ConnectionString.cs is not versioned on purposed. Add missing property file for this partial class and then run this command:
|
||||
// Add-Migration YourMigration -Project MessengerApi.Db.Npg -StartupProject MessengerApi.Db.Npg.Migrator -Verbose -Context MessengerNpgDbContext
|
||||
|
||||
// To update the database, make sure your connection string is correct and run this command:
|
||||
// Update-Database -Project MessengerApi.Db.Npg -StartupProject MessengerApi.Db.Npg.Migrator -Verbose -Context MessengerNpgDbContext
|
||||
}
|
||||
}
|
||||
}
|
||||
17
code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj
Normal file
17
code/MessengerApi.Db.Npg/MessengerApi.Db.Npg.csproj
Normal file
@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs
Normal file
31
code/MessengerApi.Db.Npg/MessengerNpgDbContext.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using MessengerApi.Db.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db.Npg
|
||||
{
|
||||
public class MessengerNpgDbContext : MessengerDbContext
|
||||
{
|
||||
private readonly string connectionString;
|
||||
|
||||
public MessengerNpgDbContext(string connectionString)
|
||||
{
|
||||
this.connectionString = connectionString;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.UseNpgsql(this.connectionString);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// https://stackoverflow.com/questions/26464357/what-is-the-purpose-of-non-unique-indexes-in-a-database
|
||||
// https://stackoverflow.com/questions/40767980/generate-a-composite-unique-constraint-index-in-ef-core
|
||||
// https://www.geeksforgeeks.org/difference-between-clustered-and-non-clustered-index/
|
||||
modelBuilder.Entity<Message>().HasIndex(e => new { e.ToId, e.IsDelivered }).IsUnique(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
123
code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs
generated
Normal file
123
code/MessengerApi.Db.Npg/Migrations/20250704170425_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,123 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MessengerApi.Db.Npg;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MessengerApi.Db.Npg.Migrations
|
||||
{
|
||||
[DbContext(typeof(MessengerNpgDbContext))]
|
||||
[Migration("20250704170425_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsAcknowledged")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDelivered")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("PayloadLifespanInSeconds")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("PayloadType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("ToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ToId", "IsDelivered");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ApiKey")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("FromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FromId");
|
||||
|
||||
b.HasIndex("ToId");
|
||||
|
||||
b.ToTable("UserRoutes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "From")
|
||||
.WithMany()
|
||||
.HasForeignKey("FromId");
|
||||
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "To")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToId");
|
||||
|
||||
b.Navigation("From");
|
||||
|
||||
b.Navigation("To");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,99 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MessengerApi.Db.Npg.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Messages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CreatedUtc = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
FromId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ToId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
IsDelivered = table.Column<bool>(type: "boolean", nullable: false),
|
||||
IsAcknowledged = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PayloadType = table.Column<string>(type: "text", nullable: true),
|
||||
Payload = table.Column<string>(type: "text", nullable: true),
|
||||
PayloadLifespanInSeconds = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Messages", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ApiKey = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: true),
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRoutes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FromId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ToId = table.Column<Guid>(type: "uuid", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRoutes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoutes_Users_FromId",
|
||||
column: x => x.FromId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoutes_Users_ToId",
|
||||
column: x => x.ToId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Messages_ToId_IsDelivered",
|
||||
table: "Messages",
|
||||
columns: new[] { "ToId", "IsDelivered" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoutes_FromId",
|
||||
table: "UserRoutes",
|
||||
column: "FromId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoutes_ToId",
|
||||
table: "UserRoutes",
|
||||
column: "ToId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Messages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRoutes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MessengerApi.Db.Npg;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MessengerApi.Db.Npg.Migrations
|
||||
{
|
||||
[DbContext(typeof(MessengerNpgDbContext))]
|
||||
partial class MessengerNpgDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("FromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsAcknowledged")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsDelivered")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("PayloadLifespanInSeconds")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("PayloadType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("ToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ToId", "IsDelivered");
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("ApiKey")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("FromId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ToId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FromId");
|
||||
|
||||
b.HasIndex("ToId");
|
||||
|
||||
b.ToTable("UserRoutes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "From")
|
||||
.WithMany()
|
||||
.HasForeignKey("FromId");
|
||||
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "To")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToId");
|
||||
|
||||
b.Navigation("From");
|
||||
|
||||
b.Navigation("To");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
|
||||
namespace MessengerApi.Db.Sql.Migrator
|
||||
{
|
||||
public partial class DesignTimeDbContextFactory : IDesignTimeDbContextFactory<MessengerSqlDbContext>
|
||||
{
|
||||
public MessengerSqlDbContext CreateDbContext(string[] args)
|
||||
{
|
||||
return new MessengerSqlDbContext(this.ConnectionString);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.6">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
16
code/MessengerApi.Db.Sql.Migrator/Program.cs
Normal file
16
code/MessengerApi.Db.Sql.Migrator/Program.cs
Normal file
@ -0,0 +1,16 @@
|
||||
namespace MessengerApi.Db.Sql.Migrator
|
||||
{
|
||||
internal class Program
|
||||
{
|
||||
static void Main(string[] args)
|
||||
{
|
||||
// You can use empty string to build the context when adding a migration - adding a migration does not hit the DB.
|
||||
|
||||
// DesignTimeDbFactory.ConnectionString.cs is not versioned on purposed. Add missing property file for this partial class and then run this command:
|
||||
// Add-Migration YourMigration -Project MessengerApi.Db.Sql -StartupProject MessengerApi.Db.Sql.Migrator -Verbose -Context MessengerSqlDbContext
|
||||
|
||||
// To update the database, make sure your connection string is correct and run this command:
|
||||
// Update-Database -Project MessengerApi.Db.Sql -StartupProject MessengerApi.Db.Sql.Migrator -Verbose -Context MessengerSqlDbContext
|
||||
}
|
||||
}
|
||||
}
|
||||
21
code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj
Normal file
21
code/MessengerApi.Db.Sql/MessengerApi.Db.Sql.csproj
Normal file
@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Migrations\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
31
code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs
Normal file
31
code/MessengerApi.Db.Sql/MessengerSqlDbContext.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using MessengerApi.Db.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db.Sql
|
||||
{
|
||||
public class MessengerSqlDbContext : MessengerDbContext
|
||||
{
|
||||
private readonly string connectionString;
|
||||
|
||||
public MessengerSqlDbContext(string connectionString)
|
||||
{
|
||||
this.connectionString = connectionString;
|
||||
}
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
optionsBuilder.UseSqlServer(this.connectionString);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
// https://stackoverflow.com/questions/26464357/what-is-the-purpose-of-non-unique-indexes-in-a-database
|
||||
// https://stackoverflow.com/questions/40767980/generate-a-composite-unique-constraint-index-in-ef-core
|
||||
// https://www.geeksforgeeks.org/difference-between-clustered-and-non-clustered-index/
|
||||
modelBuilder.Entity<Message>().HasIndex(e => new { e.ToId, e.IsDelivered }).IsUnique(false).IsClustered(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
125
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs
generated
Normal file
125
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.Designer.cs
generated
Normal file
@ -0,0 +1,125 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MessengerApi.Db.Sql;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MessengerApi.Db.Sql.Migrations
|
||||
{
|
||||
[DbContext(typeof(MessengerSqlDbContext))]
|
||||
[Migration("20250704165018_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("FromId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsAcknowledged")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDelivered")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("PayloadLifespanInSeconds")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PayloadType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("ToId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ToId", "IsDelivered");
|
||||
|
||||
SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("ToId", "IsDelivered"), false);
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ApiKey")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("FromId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ToId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FromId");
|
||||
|
||||
b.HasIndex("ToId");
|
||||
|
||||
b.ToTable("UserRoutes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "From")
|
||||
.WithMany()
|
||||
.HasForeignKey("FromId");
|
||||
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "To")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToId");
|
||||
|
||||
b.Navigation("From");
|
||||
|
||||
b.Navigation("To");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
100
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs
Normal file
100
code/MessengerApi.Db.Sql/Migrations/20250704165018_Initial.cs
Normal file
@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MessengerApi.Db.Sql.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class Initial : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Messages",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
CreatedUtc = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
FromId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ToId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
IsDelivered = table.Column<bool>(type: "bit", nullable: false),
|
||||
IsAcknowledged = table.Column<bool>(type: "bit", nullable: false),
|
||||
PayloadType = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Payload = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PayloadLifespanInSeconds = table.Column<int>(type: "int", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Messages", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Users",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
ApiKey = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
Name = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsEnabled = table.Column<bool>(type: "bit", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Users", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "UserRoutes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
FromId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
ToId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_UserRoutes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoutes_Users_FromId",
|
||||
column: x => x.FromId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_UserRoutes_Users_ToId",
|
||||
column: x => x.ToId,
|
||||
principalTable: "Users",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Messages_ToId_IsDelivered",
|
||||
table: "Messages",
|
||||
columns: new[] { "ToId", "IsDelivered" })
|
||||
.Annotation("SqlServer:Clustered", false);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoutes_FromId",
|
||||
table: "UserRoutes",
|
||||
column: "FromId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_UserRoutes_ToId",
|
||||
table: "UserRoutes",
|
||||
column: "ToId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Messages");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "UserRoutes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Users");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,122 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using MessengerApi.Db.Sql;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace MessengerApi.Db.Sql.Migrations
|
||||
{
|
||||
[DbContext(typeof(MessengerSqlDbContext))]
|
||||
partial class MessengerSqlDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.6")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||
|
||||
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.Message", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<DateTime>("CreatedUtc")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<Guid>("FromId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsAcknowledged")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<bool>("IsDelivered")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Payload")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("PayloadLifespanInSeconds")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("PayloadType")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid>("ToId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ToId", "IsDelivered");
|
||||
|
||||
SqlServerIndexBuilderExtensions.IsClustered(b.HasIndex("ToId", "IsDelivered"), false);
|
||||
|
||||
b.ToTable("Messages");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid>("ApiKey")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Users");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("FromId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.Property<Guid?>("ToId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FromId");
|
||||
|
||||
b.HasIndex("ToId");
|
||||
|
||||
b.ToTable("UserRoutes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MessengerApi.Db.Entities.UserRoute", b =>
|
||||
{
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "From")
|
||||
.WithMany()
|
||||
.HasForeignKey("FromId");
|
||||
|
||||
b.HasOne("MessengerApi.Db.Entities.User", "To")
|
||||
.WithMany()
|
||||
.HasForeignKey("ToId");
|
||||
|
||||
b.Navigation("From");
|
||||
|
||||
b.Navigation("To");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace MessengerApi.Db.Converters
|
||||
{
|
||||
public sealed class DateTimeAsUtcValueConverter()
|
||||
: ValueConverter<DateTime, DateTime>(
|
||||
v => v, v => new DateTime(v.Ticks, DateTimeKind.Utc));
|
||||
}
|
||||
19
code/MessengerApi.Db/MessengerApi.Db.csproj
Normal file
19
code/MessengerApi.Db/MessengerApi.Db.csproj
Normal file
@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.Db.Contracts\MessengerApi.Db.Contracts.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
26
code/MessengerApi.Db/MessengerDbContext.cs
Normal file
26
code/MessengerApi.Db/MessengerDbContext.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using MessengerApi.Db.Converters;
|
||||
using MessengerApi.Db.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db
|
||||
{
|
||||
public abstract class MessengerDbContext : DbContext
|
||||
{
|
||||
public DbSet<User> Users { get; set; }
|
||||
|
||||
public DbSet<Message> Messages { get; set; }
|
||||
|
||||
public DbSet<UserRoute> UserRoutes { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
modelBuilder.Entity<User>().HasKey(e => e.Id);
|
||||
modelBuilder.Entity<Message>().HasKey(e => e.Id);
|
||||
modelBuilder.Entity<Message>().Property(e => e.CreatedUtc).HasConversion<DateTimeAsUtcValueConverter>();
|
||||
modelBuilder.Entity<Message>().Property(e => e.PayloadLifespanInSeconds).IsRequired();
|
||||
modelBuilder.Entity<UserRoute>().HasKey(e => e.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
code/MessengerApi.Db/Repositories/MessageRepository.cs
Normal file
23
code/MessengerApi.Db/Repositories/MessageRepository.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using MessengerApi.Db.Contracts.Repositories;
|
||||
using MessengerApi.Db.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db.Repositories
|
||||
{
|
||||
public class MessageRepository : Repository<Message>, IMessageRepository
|
||||
{
|
||||
public MessageRepository(DbSet<Message> db) : base(db)
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<Message> GetPendingMessages(User user)
|
||||
{
|
||||
var timestamp = DateTime.UtcNow;
|
||||
|
||||
return this.db
|
||||
.Where(x => x.ToId == user.Id && x.IsDelivered == false)
|
||||
.Where(x => x.PayloadLifespanInSeconds == null || x.CreatedUtc.AddSeconds(x.PayloadLifespanInSeconds.Value) >= timestamp)
|
||||
.OrderBy(x => x.CreatedUtc);
|
||||
}
|
||||
}
|
||||
}
|
||||
26
code/MessengerApi.Db/Repositories/Repository.cs
Normal file
26
code/MessengerApi.Db/Repositories/Repository.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using MessengerApi.Db.Contracts.Entities;
|
||||
using MessengerApi.Db.Contracts.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db.Repositories
|
||||
{
|
||||
public abstract class Repository<T> : IRepository<T> where T : class, IEntity<T>
|
||||
{
|
||||
protected readonly DbSet<T> db;
|
||||
|
||||
public Repository(DbSet<T> db)
|
||||
{
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
public void Add(T entity)
|
||||
{
|
||||
this.db.Add(entity);
|
||||
}
|
||||
|
||||
public T GetById(Guid id)
|
||||
{
|
||||
return this.db.Single(x => x.Id == id);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
code/MessengerApi.Db/Repositories/UserRepository.cs
Normal file
18
code/MessengerApi.Db/Repositories/UserRepository.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using MessengerApi.Db.Contracts.Repositories;
|
||||
using MessengerApi.Db.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db.Repositories
|
||||
{
|
||||
public class UserRepository : Repository<User>, IUserRepository
|
||||
{
|
||||
public UserRepository(DbSet<User> db) : base(db)
|
||||
{
|
||||
}
|
||||
|
||||
public User SingleByApiKeyAndEnabled(Guid id, bool enabled)
|
||||
{
|
||||
return this.db.Single(x => x.ApiKey == id && x.IsEnabled == enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
code/MessengerApi.Db/Repositories/UserRouteRepository.cs
Normal file
23
code/MessengerApi.Db/Repositories/UserRouteRepository.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using MessengerApi.Db.Contracts.Repositories;
|
||||
using MessengerApi.Db.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Db.Repositories
|
||||
{
|
||||
public class UserRouteRepository : Repository<UserRoute>, IUserRouteRepository
|
||||
{
|
||||
public UserRouteRepository(DbSet<UserRoute> db) : base(db)
|
||||
{
|
||||
}
|
||||
|
||||
public IEnumerable<UserRoute> GetAllByUser(User sender)
|
||||
{
|
||||
return this.db.Include(x => x.From).Include(x => x.To).Where(x => x.From.Id == sender.Id || x.To.Id == sender.Id);
|
||||
}
|
||||
|
||||
public IEnumerable<UserRoute> GetByFrom(User user)
|
||||
{
|
||||
return this.db.Include(x => x.From).Include(x => x.To).Where(x => x.From.Id == user.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
156
code/MessengerApi.sln
Normal file
156
code/MessengerApi.sln
Normal file
@ -0,0 +1,156 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.11.35312.102
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Contracts", "MessengerApi.Contracts\MessengerApi.Contracts.csproj", "{833ED77F-A4E9-4FB3-BB84-4E55898B726A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.SubscriptionClient", "MessengerApi.SubscriptionClient\MessengerApi.SubscriptionClient.csproj", "{127D24B0-47F3-40E9-9136-899AFF206F19}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.QueryClient", "MessengerApi.QueryClient\MessengerApi.QueryClient.csproj", "{6441673B-2621-4E2C-A9A0-971E83C3F80A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{60B75400-A315-4B57-AFCF-5B4094785A62}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.PhonebookClient", "MessengerApi.Examples.PhonebookClient\MessengerApi.Examples.PhonebookClient.csproj", "{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.SnapshotSubscriptionClient", "MessengerApi.Examples.SnapshotSubscriptionClient\MessengerApi.Examples.SnapshotSubscriptionClient.csproj", "{A57429EB-3929-4E8B-B427-9B77D14CC486}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.Constants", "MessengerApi.Examples.Constants\MessengerApi.Examples.Constants.csproj", "{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Examples.QueryClient", "MessengerApi.Examples.QueryClient\MessengerApi.Examples.QueryClient.csproj", "{09DEF168-FD5C-47C3-81DF-077BE6219089}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi.Db", "MessengerApi.Db\MessengerApi.Db.csproj", "{64B33C4B-4B04-4F48-8620-4CA2AB641934}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MessengerApi", "MessengerApi\MessengerApi.csproj", "{BA717183-65C4-4568-8ACD-DEDBD2B77322}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Db", "Db", "{F318E6F5-0343-491B-9264-CFFB4CCF1241}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{6FCD97D3-1EC8-4BB0-8BE1-245B9E51565A}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Tests.LongTermSendingClient", "MessengerApi.Tests.LongTermSendingClient\MessengerApi.Tests.LongTermSendingClient.csproj", "{BAFCEB19-4FFC-44DF-8240-93172191080F}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Tests.LongTermReceivingClient", "MessengerApi.Tests.LongTermReceivingClient\MessengerApi.Tests.LongTermReceivingClient.csproj", "{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\Directory.Packages.props = ..\Directory.Packages.props
|
||||
..\docker-compose.yml = ..\docker-compose.yml
|
||||
..\Dockerfile = ..\Dockerfile
|
||||
..\NuGet.config = ..\NuGet.config
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{02EA681E-C7D8-13C7-8484-4AC65E1B71E8}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\.gitea\workflows\build.yml = ..\.gitea\workflows\build.yml
|
||||
..\.gitea\workflows\docker-build-and-push.yml = ..\.gitea\workflows\docker-build-and-push.yml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Configuration", "MessengerApi.Configuration\MessengerApi.Configuration.csproj", "{4588FB85-FD64-4B7F-B37A-6F2ADD403E80}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Sql", "MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj", "{22755F3D-C55D-436C-9C9F-C564001B976B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Npg", "MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj", "{8199D547-23AC-4B10-9BD1-2996A6C35B1D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Contracts", "MessengerApi.Db.Contracts\MessengerApi.Db.Contracts.csproj", "{062ADC2E-EF77-4319-9269-D60D39E31C0E}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Sql.Migrator", "MessengerApi.Db.Sql.Migrator\MessengerApi.Db.Sql.Migrator.csproj", "{65C395EC-81E9-4919-9721-72CAA3E4780D}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MessengerApi.Db.Npg.Migrator", "MessengerApi.Db.Npg.Migrator\MessengerApi.Db.Npg.Migrator.csproj", "{DF751DD1-9869-4916-B946-A8513A7CE706}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{833ED77F-A4E9-4FB3-BB84-4E55898B726A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{127D24B0-47F3-40E9-9136-899AFF206F19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{127D24B0-47F3-40E9-9136-899AFF206F19}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{127D24B0-47F3-40E9-9136-899AFF206F19}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{127D24B0-47F3-40E9-9136-899AFF206F19}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{6441673B-2621-4E2C-A9A0-971E83C3F80A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{6441673B-2621-4E2C-A9A0-971E83C3F80A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6441673B-2621-4E2C-A9A0-971E83C3F80A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6441673B-2621-4E2C-A9A0-971E83C3F80A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A57429EB-3929-4E8B-B427-9B77D14CC486}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A57429EB-3929-4E8B-B427-9B77D14CC486}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A57429EB-3929-4E8B-B427-9B77D14CC486}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A57429EB-3929-4E8B-B427-9B77D14CC486}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{09DEF168-FD5C-47C3-81DF-077BE6219089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{09DEF168-FD5C-47C3-81DF-077BE6219089}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{09DEF168-FD5C-47C3-81DF-077BE6219089}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{09DEF168-FD5C-47C3-81DF-077BE6219089}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{64B33C4B-4B04-4F48-8620-4CA2AB641934}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{64B33C4B-4B04-4F48-8620-4CA2AB641934}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{64B33C4B-4B04-4F48-8620-4CA2AB641934}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{64B33C4B-4B04-4F48-8620-4CA2AB641934}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BA717183-65C4-4568-8ACD-DEDBD2B77322}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BA717183-65C4-4568-8ACD-DEDBD2B77322}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BA717183-65C4-4568-8ACD-DEDBD2B77322}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BA717183-65C4-4568-8ACD-DEDBD2B77322}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{BAFCEB19-4FFC-44DF-8240-93172191080F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{BAFCEB19-4FFC-44DF-8240-93172191080F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{BAFCEB19-4FFC-44DF-8240-93172191080F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{BAFCEB19-4FFC-44DF-8240-93172191080F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4588FB85-FD64-4B7F-B37A-6F2ADD403E80}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{22755F3D-C55D-436C-9C9F-C564001B976B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{22755F3D-C55D-436C-9C9F-C564001B976B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{22755F3D-C55D-436C-9C9F-C564001B976B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{22755F3D-C55D-436C-9C9F-C564001B976B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8199D547-23AC-4B10-9BD1-2996A6C35B1D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{062ADC2E-EF77-4319-9269-D60D39E31C0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{062ADC2E-EF77-4319-9269-D60D39E31C0E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{062ADC2E-EF77-4319-9269-D60D39E31C0E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{062ADC2E-EF77-4319-9269-D60D39E31C0E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{65C395EC-81E9-4919-9721-72CAA3E4780D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{65C395EC-81E9-4919-9721-72CAA3E4780D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{65C395EC-81E9-4919-9721-72CAA3E4780D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{65C395EC-81E9-4919-9721-72CAA3E4780D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DF751DD1-9869-4916-B946-A8513A7CE706}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DF751DD1-9869-4916-B946-A8513A7CE706}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DF751DD1-9869-4916-B946-A8513A7CE706}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DF751DD1-9869-4916-B946-A8513A7CE706}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{D2C7396F-B0CA-4BA5-A07A-C70DB283B3CF} = {60B75400-A315-4B57-AFCF-5B4094785A62}
|
||||
{A57429EB-3929-4E8B-B427-9B77D14CC486} = {60B75400-A315-4B57-AFCF-5B4094785A62}
|
||||
{7EC1857B-5BFD-46F6-809D-CE617CFD9A8C} = {60B75400-A315-4B57-AFCF-5B4094785A62}
|
||||
{09DEF168-FD5C-47C3-81DF-077BE6219089} = {60B75400-A315-4B57-AFCF-5B4094785A62}
|
||||
{64B33C4B-4B04-4F48-8620-4CA2AB641934} = {F318E6F5-0343-491B-9264-CFFB4CCF1241}
|
||||
{BAFCEB19-4FFC-44DF-8240-93172191080F} = {6FCD97D3-1EC8-4BB0-8BE1-245B9E51565A}
|
||||
{FE628370-BD9E-4745-8C5B-EDAA44BBA2BB} = {6FCD97D3-1EC8-4BB0-8BE1-245B9E51565A}
|
||||
{02EA681E-C7D8-13C7-8484-4AC65E1B71E8} = {8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}
|
||||
{22755F3D-C55D-436C-9C9F-C564001B976B} = {F318E6F5-0343-491B-9264-CFFB4CCF1241}
|
||||
{8199D547-23AC-4B10-9BD1-2996A6C35B1D} = {F318E6F5-0343-491B-9264-CFFB4CCF1241}
|
||||
{062ADC2E-EF77-4319-9269-D60D39E31C0E} = {F318E6F5-0343-491B-9264-CFFB4CCF1241}
|
||||
{65C395EC-81E9-4919-9721-72CAA3E4780D} = {F318E6F5-0343-491B-9264-CFFB4CCF1241}
|
||||
{DF751DD1-9869-4916-B946-A8513A7CE706} = {F318E6F5-0343-491B-9264-CFFB4CCF1241}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {61948E36-4C2B-4BC9-80B6-9E155CE9F7DE}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
13
code/MessengerApi/.config/dotnet-tools.json
Normal file
13
code/MessengerApi/.config/dotnet-tools.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "8.0.10",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
7
code/MessengerApi/Constants.cs
Normal file
7
code/MessengerApi/Constants.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MessengerApi
|
||||
{
|
||||
public class Constants
|
||||
{
|
||||
public const string USERFILE_FILENAME = "/app/users.conf";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using MessengerApi.Db;
|
||||
|
||||
namespace MessengerApi.Contracts.Factories
|
||||
{
|
||||
public interface IDbContextFactory
|
||||
{
|
||||
MessengerDbContext CreateDbContext();
|
||||
}
|
||||
}
|
||||
15
code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs
Normal file
15
code/MessengerApi/Contracts/Models/Scoped/IUnitOfWork.cs
Normal file
@ -0,0 +1,15 @@
|
||||
using MessengerApi.Db.Contracts.Repositories;
|
||||
|
||||
namespace MessengerApi.Contracts.Models.Scoped
|
||||
{
|
||||
public interface IUnitOfWork
|
||||
{
|
||||
IUserRepository Users { get; }
|
||||
|
||||
IUserRouteRepository UserRoutes { get; }
|
||||
|
||||
IMessageRepository Messages { get; }
|
||||
|
||||
Task SaveChanges(CancellationToken ct = default);
|
||||
}
|
||||
}
|
||||
8
code/MessengerApi/Dockerfile
Normal file
8
code/MessengerApi/Dockerfile
Normal file
@ -0,0 +1,8 @@
|
||||
#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
|
||||
USER app
|
||||
WORKDIR /app
|
||||
EXPOSE 8080
|
||||
COPY ./publish .
|
||||
ENTRYPOINT ["dotnet", "MessengerApi.dll"]
|
||||
34
code/MessengerApi/Factories/DbContextFactory.cs
Normal file
34
code/MessengerApi/Factories/DbContextFactory.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using MessengerApi.Configuration.Model;
|
||||
using MessengerApi.Configuration.Model.Persistence;
|
||||
using MessengerApi.Contracts.Factories;
|
||||
using MessengerApi.Db;
|
||||
using MessengerApi.Db.Npg;
|
||||
using MessengerApi.Db.Sql;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Factories
|
||||
{
|
||||
public class DbContextFactory : IDbContextFactory, IDbContextFactory<MessengerDbContext>
|
||||
{
|
||||
private readonly MessengerConfiguration configuration;
|
||||
|
||||
public DbContextFactory(MessengerConfiguration configuration)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public MessengerDbContext CreateDbContext()
|
||||
{
|
||||
if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.Sql)
|
||||
{
|
||||
return new MessengerSqlDbContext((configuration.PersistenceConfiguration as SqlPersistenceConfiguration).ConnectionString);
|
||||
}
|
||||
else if (this.configuration.PersistenceConfiguration.PersistenceType == Configuration.Enums.PersistenceTypes.PostgreSql)
|
||||
{
|
||||
return new MessengerNpgDbContext((configuration.PersistenceConfiguration as NpgPersistenceConfiguration).ConnectionString);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
code/MessengerApi/Factories/LoggerFactory.cs
Normal file
32
code/MessengerApi/Factories/LoggerFactory.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using MessengerApi.Configuration.Model;
|
||||
|
||||
namespace MessengerApi.Factories
|
||||
{
|
||||
public class LoggerFactory : IServiceProvider
|
||||
{
|
||||
private readonly MessengerConfiguration _configuration;
|
||||
|
||||
public LoggerFactory(MessengerConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public ILogger CreateLogger()
|
||||
{
|
||||
var logger = new ConsoleLogger()
|
||||
{
|
||||
IsDebugOutputEnabled = (this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Debug || this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Trace)
|
||||
? true : false,
|
||||
IsTraceOutputEnabled = this._configuration.Verbosity == Configuration.Enums.LoggingVerbosity.Trace
|
||||
? true : false
|
||||
};
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
public object GetService(Type serviceType)
|
||||
{
|
||||
return this.CreateLogger();
|
||||
}
|
||||
}
|
||||
}
|
||||
2
code/MessengerApi/GlobalUsings.cs
Normal file
2
code/MessengerApi/GlobalUsings.cs
Normal file
@ -0,0 +1,2 @@
|
||||
global using portaloggy;
|
||||
global using ILogger = portaloggy.ILogger;
|
||||
@ -0,0 +1,81 @@
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Models;
|
||||
using MessengerApi.Models.Scoped;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Encodings.Web;
|
||||
|
||||
namespace MessengerApi.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates our permananet API keys sent over as Bearer tokens.
|
||||
/// </summary>
|
||||
public class CustomBearerAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
|
||||
{
|
||||
private readonly IMemoryCache memoryCache;
|
||||
|
||||
public CustomBearerAuthenticationHandler(
|
||||
IOptionsMonitor<AuthenticationSchemeOptions> options,
|
||||
ILoggerFactory loggerFactory,
|
||||
UrlEncoder encoder,
|
||||
IMemoryCache memoryCache)
|
||||
: base(options, loggerFactory, encoder)
|
||||
{
|
||||
this.memoryCache = memoryCache;
|
||||
}
|
||||
|
||||
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
|
||||
{
|
||||
const string HEADER = "Authorization";
|
||||
const string PREFIX = "Bearer ";
|
||||
|
||||
Context.RequestServices.GetRequiredService<Timing>(); // creates the object in scope.
|
||||
|
||||
if (!Request.Headers.TryGetValue(HEADER, out var authHeader) ||
|
||||
!authHeader.ToString().StartsWith(PREFIX))
|
||||
{
|
||||
return Task.FromResult(AuthenticateResult.NoResult());
|
||||
}
|
||||
|
||||
var token = authHeader.ToString().Substring(PREFIX.Length).Trim();
|
||||
|
||||
if(this.memoryCache.TryGetValue(token, out CachedIdentity oldCache))
|
||||
{
|
||||
var identity = Context.RequestServices.GetRequiredService<Identity>();
|
||||
identity.User = oldCache.User;
|
||||
identity.UserRoutes = oldCache.UserRoutes;
|
||||
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(oldCache.ClaimsPrincipal, Scheme.Name)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var unitOfWork = Context.RequestServices.GetRequiredService<IUnitOfWork>();
|
||||
var user = unitOfWork.Users.SingleByApiKeyAndEnabled(Guid.Parse(token), true);
|
||||
var routes = unitOfWork.UserRoutes.GetAllByUser(user).ToArray();
|
||||
|
||||
var principal = new ClaimsPrincipal(
|
||||
new ClaimsIdentity(
|
||||
new List<Claim>
|
||||
{
|
||||
new Claim(ClaimTypes.NameIdentifier, user.Name),
|
||||
new Claim(ClaimTypes.Name, user.Name)
|
||||
}, Scheme.Name));
|
||||
|
||||
var cache = new CachedIdentity
|
||||
{
|
||||
ClaimsPrincipal = principal,
|
||||
User = user,
|
||||
UserRoutes = routes
|
||||
};
|
||||
|
||||
this.memoryCache.Set(token, cache, TimeSpan.FromMinutes(5));
|
||||
|
||||
var identity = Context.RequestServices.GetRequiredService<Identity>();
|
||||
identity.User = cache.User;
|
||||
identity.UserRoutes = cache.UserRoutes;
|
||||
return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(cache.ClaimsPrincipal, Scheme.Name)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
40
code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs
Normal file
40
code/MessengerApi/Handlers/Endpoint/AckEndpointHandler.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Models.Scoped;
|
||||
|
||||
namespace MessengerApi.Handlers.Endpoint
|
||||
{
|
||||
public class AckEndpointHandler
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly IUnitOfWork unitOfWork;
|
||||
private readonly Identity identity;
|
||||
|
||||
public AckEndpointHandler(
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
Identity identity)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.unitOfWork = unitOfWork;
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
public async Task AckMessage(Guid messageId)
|
||||
{
|
||||
var message = unitOfWork.Messages.GetById(messageId);
|
||||
|
||||
// Authorize.
|
||||
if (message.ToId != this.identity.User.Id)
|
||||
{
|
||||
throw new InvalidOperationException("It's not your message to ack.");
|
||||
}
|
||||
else if(!message.IsDelivered)
|
||||
{
|
||||
throw new InvalidOperationException("Can't ack undelivered message.");
|
||||
}
|
||||
|
||||
// Act.
|
||||
message.IsAcknowledged = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs
Normal file
35
code/MessengerApi/Handlers/Endpoint/PeekEndpointHandler.cs
Normal file
@ -0,0 +1,35 @@
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Models.Scoped;
|
||||
|
||||
namespace MessengerApi.Handlers.Endpoint
|
||||
{
|
||||
public class PeekEndpointHandler
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly Timing timing;
|
||||
private readonly Identity identity;
|
||||
private readonly IUnitOfWork unitOfWork;
|
||||
|
||||
public PeekEndpointHandler(
|
||||
ILogger logger,
|
||||
Timing timing,
|
||||
Identity identity,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.timing = timing;
|
||||
this.identity = identity;
|
||||
this.unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public Task<int> Peek()
|
||||
{
|
||||
var pendingMessages = this.unitOfWork.Messages.GetPendingMessages(this.identity.User);
|
||||
|
||||
this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is receiving {pendingMessages.Count()}.");
|
||||
|
||||
return Task.FromResult(pendingMessages.Count());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Db.Entities;
|
||||
using MessengerApi.Models.Scoped;
|
||||
|
||||
namespace MessengerApi.Handlers.Endpoint
|
||||
{
|
||||
public class ReceiveEndpointHandler
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly Timing timing;
|
||||
private readonly Identity identity;
|
||||
private readonly IUnitOfWork unitOfWork;
|
||||
|
||||
public ReceiveEndpointHandler(
|
||||
ILogger logger,
|
||||
Timing timing,
|
||||
Identity identity,
|
||||
IUnitOfWork unitOfWork)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.timing = timing;
|
||||
this.identity = identity;
|
||||
this.unitOfWork = unitOfWork;
|
||||
}
|
||||
|
||||
public Task<Message[]> ReceiveMessages()
|
||||
{
|
||||
var pendingMessages = this.unitOfWork.Messages.GetPendingMessages(this.identity.User);
|
||||
|
||||
this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is receiving {pendingMessages.Count()}.");
|
||||
|
||||
if (!pendingMessages.Any())
|
||||
{
|
||||
return Task.FromResult(new Message[0]);
|
||||
}
|
||||
|
||||
var messages = pendingMessages.ToList();
|
||||
messages.ForEach(x => x.IsDelivered = true);
|
||||
return Task.FromResult(messages.ToArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
60
code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs
Normal file
60
code/MessengerApi/Handlers/Endpoint/SendEndpointHandler.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using MessengerApi.Configuration.Model;
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Db.Entities;
|
||||
using MessengerApi.Models.Scoped;
|
||||
|
||||
namespace MessengerApi.Handlers.Endpoint
|
||||
{
|
||||
public class SendEndpointHandler
|
||||
{
|
||||
private readonly MessengerConfiguration configuration;
|
||||
private readonly ILogger logger;
|
||||
|
||||
private readonly Timing timing;
|
||||
private readonly Identity identity;
|
||||
private readonly IUnitOfWork unitOfWork;
|
||||
|
||||
public SendEndpointHandler(
|
||||
MessengerConfiguration configuration,
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
Timing timing,
|
||||
Identity identity)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.logger = logger;
|
||||
this.unitOfWork = unitOfWork;
|
||||
this.timing = timing;
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
public Task<Message> SendMessage(
|
||||
Guid? toUserId,
|
||||
string payload,
|
||||
string payloadType,
|
||||
int? payloadLifespanInSeconds)
|
||||
{
|
||||
// Authorize.
|
||||
var targetRecipientId = toUserId.HasValue
|
||||
? this.identity.UserRoutes.Single(x => x.From.Id == this.identity.User.Id && x.To.Id == toUserId.Value).To.Id
|
||||
: this.identity.UserRoutes.Single().To.Id;
|
||||
|
||||
this.logger.Debug($"[{this.timing.Timestamp:s}] User {this.identity.User.Name} is authorized to send message to {targetRecipientId}.");
|
||||
|
||||
// Act.
|
||||
var message = new Message
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CreatedUtc = this.timing.Timestamp,
|
||||
FromId = this.identity.User.Id,
|
||||
ToId = targetRecipientId,
|
||||
Payload = payload,
|
||||
PayloadType = payloadType,
|
||||
PayloadLifespanInSeconds = payloadLifespanInSeconds ?? (this.configuration.DefaultMessageLifetimeInMinutes * 60)
|
||||
};
|
||||
|
||||
this.unitOfWork.Messages.Add(message);
|
||||
return Task.FromResult(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
47
code/MessengerApi/Handlers/HousekeepingHandler.cs
Normal file
47
code/MessengerApi/Handlers/HousekeepingHandler.cs
Normal file
@ -0,0 +1,47 @@
|
||||
using MessengerApi.Configuration.Model;
|
||||
using MessengerApi.Contracts.Factories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace MessengerApi.Handlers
|
||||
{
|
||||
public class HousekeepingHandler
|
||||
{
|
||||
private readonly ILogger logger;
|
||||
private readonly MessengerConfiguration configuration;
|
||||
private readonly IDbContextFactory dbContextFactory;
|
||||
|
||||
public HousekeepingHandler(
|
||||
ILogger logger,
|
||||
IDbContextFactory dbContextFactory,
|
||||
MessengerConfiguration configuration)
|
||||
{
|
||||
this.logger = logger;
|
||||
this.dbContextFactory = dbContextFactory;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task RemoveOldMessages()
|
||||
{
|
||||
this.logger.Trace($"Executing {nameof(this.RemoveOldMessages)}.");
|
||||
|
||||
var timestamp = DateTime.UtcNow;
|
||||
var cutoff = timestamp.AddMinutes(-this.configuration.HousekeepingMessageAgeInMinutes);
|
||||
using var ctx = this.dbContextFactory.CreateDbContext();
|
||||
await ctx.Messages.Where(x => x.CreatedUtc < cutoff).ExecuteDeleteAsync();
|
||||
|
||||
if (this.configuration.HousekeepingMessageState != Configuration.Enums.HousekeepingMessageStates.None)
|
||||
{
|
||||
this.logger.Trace($"Executing additional message state cleaning in {nameof(this.RemoveOldMessages)}.");
|
||||
|
||||
if (this.configuration.HousekeepingMessageState == Configuration.Enums.HousekeepingMessageStates.Delivered)
|
||||
{
|
||||
await ctx.Messages.Where(x => x.IsDelivered).ExecuteDeleteAsync();
|
||||
}
|
||||
else if (this.configuration.HousekeepingMessageState == Configuration.Enums.HousekeepingMessageStates.Acknowledged)
|
||||
{
|
||||
await ctx.Messages.Where(x => x.IsAcknowledged).ExecuteDeleteAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
99
code/MessengerApi/Handlers/UserSetupHandler.cs
Normal file
99
code/MessengerApi/Handlers/UserSetupHandler.cs
Normal file
@ -0,0 +1,99 @@
|
||||
using MessengerApi.Configuration.Model;
|
||||
using MessengerApi.Contracts.Factories;
|
||||
using MessengerApi.Db;
|
||||
using MessengerApi.Models;
|
||||
using System.Text;
|
||||
|
||||
namespace MessengerApi.Handlers
|
||||
{
|
||||
// TODO: This needs to be redone, because at every run, it wipes users and creates new ones. This makes
|
||||
// all existing DB messages unassignable.
|
||||
public class UserSetupHandler
|
||||
{
|
||||
private readonly MessengerConfiguration configuration;
|
||||
private readonly ILogger logger;
|
||||
private readonly IDbContextFactory dbContextFactory;
|
||||
|
||||
public UserSetupHandler(
|
||||
MessengerConfiguration configuration,
|
||||
ILogger logger,
|
||||
IDbContextFactory dbContextFactory)
|
||||
{
|
||||
this.configuration = configuration;
|
||||
this.logger = logger;
|
||||
this.dbContextFactory = dbContextFactory;
|
||||
}
|
||||
|
||||
public async Task UpdateFromFile(FileInfo file)
|
||||
{
|
||||
if(file.Exists)
|
||||
{
|
||||
var lines = await File.ReadAllLinesAsync(file.FullName, Encoding.UTF8);
|
||||
var items = await this.ReadLines(lines);
|
||||
|
||||
await this.SynchronizeUsers(items);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<UserSetupItem[]> ReadLines(string[] lines)
|
||||
{
|
||||
var items = new List<UserSetupItem>();
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var values = line.Split(';', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
var item = new UserSetupItem
|
||||
{
|
||||
UserName = values[0],
|
||||
ApiKey = values[1],
|
||||
};
|
||||
|
||||
if(values.Length > 2)
|
||||
{
|
||||
item.CanSendToUserNames = values[2].Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||
}
|
||||
|
||||
items.Add(item);
|
||||
}
|
||||
|
||||
if (items.GroupBy(x => x.UserName).Any(x => x.Count() > 1))
|
||||
{
|
||||
throw new InvalidOperationException("Usernames are not unique. One username per line.");
|
||||
}
|
||||
else if(items.GroupBy(x=>x.ApiKey).Any(x=>x.Count() > 1))
|
||||
{
|
||||
throw new InvalidOperationException("API keys are not unique. One API key per line.");
|
||||
}
|
||||
|
||||
return items.ToArray();
|
||||
}
|
||||
|
||||
private Task SynchronizeUsers(IEnumerable<UserSetupItem> users)
|
||||
{
|
||||
using var db = this.dbContextFactory.CreateDbContext();
|
||||
db.RemoveRange(db.Users);
|
||||
db.RemoveRange(db.UserRoutes);
|
||||
|
||||
var dbUsers = users.Select(x => new Db.Entities.User
|
||||
{
|
||||
Id = new Guid(),
|
||||
Name = x.UserName,
|
||||
ApiKey = Guid.Parse(x.ApiKey),
|
||||
IsEnabled = true
|
||||
});
|
||||
|
||||
var dbRoutes = users.SelectMany(x => x.CanSendToUserNames.Select(cs => new Db.Entities.UserRoute
|
||||
{
|
||||
Id = new Guid(),
|
||||
From = dbUsers.Single(dbu => dbu.Name == x.UserName),
|
||||
To = dbUsers.Single(dbu => dbu.Name == x.UserName)
|
||||
}));
|
||||
|
||||
db.AddRange(dbUsers);
|
||||
db.AddRange(dbRoutes);
|
||||
|
||||
db.SaveChanges();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
30
code/MessengerApi/MessengerApi.csproj
Normal file
30
code/MessengerApi/MessengerApi.csproj
Normal file
@ -0,0 +1,30 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>disable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>85c81e87-1274-45ce-8b91-6d6619ffdfa2</UserSecretsId>
|
||||
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
|
||||
<BaseOutputPath>..\out\</BaseOutputPath>
|
||||
<StartupObject>MessengerApi.Api.Program</StartupObject>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
<PackageReference Include="portaloggy" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MessengerApi.Configuration\MessengerApi.Configuration.csproj" />
|
||||
<ProjectReference Include="..\MessengerApi.Contracts\MessengerApi.Contracts.csproj" />
|
||||
<ProjectReference Include="..\MessengerApi.Db.Npg\MessengerApi.Db.Npg.csproj" />
|
||||
<ProjectReference Include="..\MessengerApi.Db.Sql\MessengerApi.Db.Sql.csproj" />
|
||||
<ProjectReference Include="..\MessengerApi.Db\MessengerApi.Db.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
13
code/MessengerApi/Models/CachedIdentity.cs
Normal file
13
code/MessengerApi/Models/CachedIdentity.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace MessengerApi.Models
|
||||
{
|
||||
public class CachedIdentity
|
||||
{
|
||||
public Db.Entities.User User { get; set; }
|
||||
|
||||
public Db.Entities.UserRoute[] UserRoutes { get; set; }
|
||||
|
||||
public ClaimsPrincipal ClaimsPrincipal { get; set; }
|
||||
}
|
||||
}
|
||||
7
code/MessengerApi/Models/Http/AckRequest.cs
Normal file
7
code/MessengerApi/Models/Http/AckRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MessengerApi.Models.Http
|
||||
{
|
||||
public class AckRequest
|
||||
{
|
||||
public Guid MessageId { get; set; }
|
||||
}
|
||||
}
|
||||
13
code/MessengerApi/Models/Http/SendRequest.cs
Normal file
13
code/MessengerApi/Models/Http/SendRequest.cs
Normal file
@ -0,0 +1,13 @@
|
||||
namespace MessengerApi.Models.Http
|
||||
{
|
||||
public class SendRequest
|
||||
{
|
||||
public Guid? ToUserId { get; set; }
|
||||
|
||||
public string Payload { get; set; }
|
||||
|
||||
public string PayloadType { get; set; }
|
||||
|
||||
public int? PayloadLifetimeInSeconds { get; set; }
|
||||
}
|
||||
}
|
||||
7
code/MessengerApi/Models/Http/VerifyRequest.cs
Normal file
7
code/MessengerApi/Models/Http/VerifyRequest.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MessengerApi.Models.Http
|
||||
{
|
||||
public class VerifyRequest
|
||||
{
|
||||
public Guid MessageId { get; set; }
|
||||
}
|
||||
}
|
||||
9
code/MessengerApi/Models/Scoped/Identity.cs
Normal file
9
code/MessengerApi/Models/Scoped/Identity.cs
Normal file
@ -0,0 +1,9 @@
|
||||
namespace MessengerApi.Models.Scoped
|
||||
{
|
||||
public class Identity
|
||||
{
|
||||
public Db.Entities.User User { get; set; }
|
||||
|
||||
public Db.Entities.UserRoute[] UserRoutes { get; set; }
|
||||
}
|
||||
}
|
||||
7
code/MessengerApi/Models/Scoped/Timing.cs
Normal file
7
code/MessengerApi/Models/Scoped/Timing.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace MessengerApi.Models.Scoped
|
||||
{
|
||||
public class Timing
|
||||
{
|
||||
public DateTime Timestamp { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
33
code/MessengerApi/Models/Scoped/UnitOfWork.cs
Normal file
33
code/MessengerApi/Models/Scoped/UnitOfWork.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using MessengerApi.Contracts.Factories;
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Db;
|
||||
using MessengerApi.Db.Contracts.Repositories;
|
||||
using MessengerApi.Db.Repositories;
|
||||
|
||||
namespace MessengerApi.Models.Scoped
|
||||
{
|
||||
public class UnitOfWork : IUnitOfWork
|
||||
{
|
||||
private MessengerDbContext context;
|
||||
|
||||
public IUserRepository Users { get; }
|
||||
|
||||
public IUserRouteRepository UserRoutes { get; }
|
||||
|
||||
public IMessageRepository Messages { get; }
|
||||
|
||||
public UnitOfWork(
|
||||
IDbContextFactory dbContextFactory)
|
||||
{
|
||||
this.context = dbContextFactory.CreateDbContext();
|
||||
this.Users = new UserRepository(this.context.Users);
|
||||
this.UserRoutes = new UserRouteRepository(this.context.UserRoutes);
|
||||
this.Messages = new MessageRepository(this.context.Messages);
|
||||
}
|
||||
|
||||
public Task SaveChanges(CancellationToken ct = default)
|
||||
{
|
||||
return this.context.SaveChangesAsync(ct);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
code/MessengerApi/Models/UserSetupItem.cs
Normal file
11
code/MessengerApi/Models/UserSetupItem.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace MessengerApi.Models
|
||||
{
|
||||
public class UserSetupItem
|
||||
{
|
||||
public string UserName { get; set; }
|
||||
|
||||
public string ApiKey { get; set; }
|
||||
|
||||
public string[] CanSendToUserNames { get; set; }
|
||||
}
|
||||
}
|
||||
313
code/MessengerApi/Program.cs
Normal file
313
code/MessengerApi/Program.cs
Normal file
@ -0,0 +1,313 @@
|
||||
using MessengerApi.Configuration.Model;
|
||||
using MessengerApi.Configuration.Model.Persistence;
|
||||
using MessengerApi.Configuration.Sources.Environment;
|
||||
using MessengerApi.Contracts.Factories;
|
||||
using MessengerApi.Contracts.Models.Scoped;
|
||||
using MessengerApi.Db;
|
||||
using MessengerApi.Factories;
|
||||
using MessengerApi.Handlers;
|
||||
using MessengerApi.Handlers.Endpoint;
|
||||
using MessengerApi.Models.Http;
|
||||
using MessengerApi.Models.Scoped;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Net;
|
||||
using System.Threading.RateLimiting;
|
||||
|
||||
namespace MessengerApi.Api
|
||||
{
|
||||
public class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
MessengerConfiguration configuration = null;
|
||||
|
||||
try
|
||||
{
|
||||
configuration = new MessengerConfiguration(new EnvironmentConfigurationSource());
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine("Can't load settings.", ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Configuration.AddEnvironmentVariables();
|
||||
|
||||
builder.Services.AddMemoryCache();
|
||||
builder.Services.AddSingleton<MessengerConfiguration>(configuration);
|
||||
builder.Services.AddSingleton<ILogger>(new Factories.LoggerFactory(configuration).CreateLogger());
|
||||
builder.Services.AddSingleton<SendEndpointHandler>();
|
||||
builder.Services.AddSingleton<HousekeepingHandler>();
|
||||
builder.Services.AddSingleton<UserSetupHandler>();
|
||||
builder.Services.AddSingleton<IDbContextFactory, DbContextFactory>();
|
||||
|
||||
builder.Services.AddScoped<Timing>();
|
||||
builder.Services.AddScoped<Identity>();
|
||||
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
builder.Services.AddScoped<SendEndpointHandler>();
|
||||
builder.Services.AddScoped<ReceiveEndpointHandler>();
|
||||
builder.Services.AddScoped<AckEndpointHandler>();
|
||||
builder.Services.AddScoped<PeekEndpointHandler>();
|
||||
|
||||
// Authentication.
|
||||
builder.Services
|
||||
.AddAuthentication("Bearer")
|
||||
.AddScheme<AuthenticationSchemeOptions, CustomBearerAuthenticationHandler>("Bearer", null);
|
||||
|
||||
// CORS.
|
||||
builder.Services
|
||||
.AddCors(opt => opt.AddPolicy("originpolicy", builder =>
|
||||
{
|
||||
builder
|
||||
.WithOrigins(configuration.Origins.ToArray())
|
||||
.AllowAnyHeader()
|
||||
.AllowAnyMethod()
|
||||
.AllowCredentials();
|
||||
}));
|
||||
|
||||
// Ratelimiting
|
||||
builder.Services.AddRateLimiter(options =>
|
||||
{
|
||||
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
|
||||
{
|
||||
var key = httpContext.Request.Headers["Authorization"].FirstOrDefault()
|
||||
?? "anonymous";
|
||||
|
||||
return RateLimitPartition.GetFixedWindowLimiter(key, _ => new FixedWindowRateLimiterOptions
|
||||
{
|
||||
PermitLimit = configuration.RateLimitPerMinute,
|
||||
Window = TimeSpan.FromMinutes(1),
|
||||
QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
|
||||
QueueLimit = 0
|
||||
});
|
||||
});
|
||||
|
||||
options.RejectionStatusCode = 429;
|
||||
});
|
||||
|
||||
// Proxy registration to forward real client IPs.
|
||||
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
||||
{
|
||||
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
||||
|
||||
foreach (var proxy in configuration.Proxies)
|
||||
{
|
||||
options.KnownProxies.Add(IPAddress.Parse(proxy));
|
||||
}
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
app.UseDeveloperExceptionPage();
|
||||
|
||||
// DB Migrations
|
||||
using (var ctx = app.Services.GetRequiredService<IDbContextFactory>().CreateDbContext())
|
||||
{
|
||||
var migrationLogger = app.Services.GetRequiredService<ILogger>();
|
||||
|
||||
try
|
||||
{
|
||||
if (ctx.Database.GetPendingMigrations().Any())
|
||||
{
|
||||
migrationLogger.Info("Applying migrations.");
|
||||
ctx.Database.Migrate();
|
||||
}
|
||||
else
|
||||
{
|
||||
migrationLogger.Info("No migrations pending.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
migrationLogger.Error("Can't run migrations successfully.", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
// Housekeeping.
|
||||
if (configuration.HousekeepingEnabled)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
await app.Services.GetService<HousekeepingHandler>().RemoveOldMessages();
|
||||
await Task.Delay(TimeSpan.FromMinutes(1));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// User synchronization
|
||||
var userSetupHandler = app.Services.GetRequiredService<UserSetupHandler>();
|
||||
userSetupHandler.UpdateFromFile(new FileInfo(Constants.USERFILE_FILENAME)).GetAwaiter().GetResult();
|
||||
|
||||
app.UseStaticFiles();
|
||||
app.UseRouting();
|
||||
app.UseAuthentication();
|
||||
app.UseCors("originpolicy");
|
||||
app.UseForwardedHeaders();
|
||||
|
||||
// Ray id logging.
|
||||
app.Use(async (context, next) =>
|
||||
{
|
||||
var stamp = DateTime.UtcNow;
|
||||
var logger = context.RequestServices.GetRequiredService<ILogger>();
|
||||
var ipa = context?.Connection?.RemoteIpAddress?.ToString() ?? "unknown";
|
||||
var uid = context?.User?.Identity?.Name ?? "unknown";
|
||||
var una = context?.User?.Claims?.SingleOrDefault(x => x.Type == "UserName")?.Value ?? "unknown";
|
||||
var rid = context?.TraceIdentifier ?? "unknown";
|
||||
var endpoint = context?.GetEndpoint()?.DisplayName ?? "unknown";
|
||||
|
||||
logger.Info($"{endpoint} call {rid}; ip {ipa}; u {una}/{uid}");
|
||||
|
||||
await next();
|
||||
});
|
||||
|
||||
app.UseRateLimiter();
|
||||
|
||||
// Endpoint registration.
|
||||
app.MapPost("/send", async (
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
SendEndpointHandler handler,
|
||||
[FromBody] SendRequest request) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await handler.SendMessage(request.ToUserId, request.Payload, request.PayloadType, request.PayloadLifetimeInSeconds);
|
||||
await unitOfWork.SaveChanges();
|
||||
return Results.Json(response.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("Can't send.", ex);
|
||||
return Results.InternalServerError();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/receive", async (
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
ReceiveEndpointHandler handler) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = await handler.ReceiveMessages();
|
||||
|
||||
if (messages?.Any() != true)
|
||||
{
|
||||
return Results.NoContent();
|
||||
}
|
||||
else
|
||||
{
|
||||
await unitOfWork.SaveChanges();
|
||||
|
||||
return Results.Json(new
|
||||
{
|
||||
Messages = messages.Select(x => new
|
||||
{
|
||||
Id = x.Id,
|
||||
TimestampUtc = x.CreatedUtc,
|
||||
Payload = x.Payload,
|
||||
PayloadType = x.PayloadType,
|
||||
Sender = x.FromId
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("Can't send.", ex);
|
||||
return Results.InternalServerError();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapPost("/ack", async (
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
AckEndpointHandler handler,
|
||||
AckRequest request) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await handler.AckMessage(request.MessageId);
|
||||
await unitOfWork.SaveChanges();
|
||||
return Results.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("Can't send.", ex);
|
||||
return Results.InternalServerError();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/yellowpages", (
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
Identity identity) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var routes = unitOfWork.UserRoutes.GetByFrom(identity.User).ToList();
|
||||
return Results.Json(new
|
||||
{
|
||||
Users = routes.Select(x => new
|
||||
{
|
||||
Id = x.To.Id,
|
||||
Name = x.To.Name
|
||||
})
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("Can't yellowpages.", ex);
|
||||
return Results.InternalServerError();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/peek", async (
|
||||
ILogger logger,
|
||||
PeekEndpointHandler handler) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var pending = await handler.Peek();
|
||||
return Results.Json(pending);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("Can't peek.", ex);
|
||||
return Results.InternalServerError();
|
||||
}
|
||||
});
|
||||
|
||||
app.MapGet("/verify", (
|
||||
ILogger logger,
|
||||
IUnitOfWork unitOfWork,
|
||||
Guid messageId) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var message = unitOfWork.Messages.GetById(messageId);
|
||||
|
||||
return Results.Json(new
|
||||
{
|
||||
message.IsDelivered,
|
||||
message.IsAcknowledged
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error("Can't verify.", ex);
|
||||
return Results.InternalServerError();
|
||||
}
|
||||
});
|
||||
|
||||
app.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
28
code/MessengerApi/Properties/launchSettings.json
Normal file
28
code/MessengerApi/Properties/launchSettings.json
Normal file
@ -0,0 +1,28 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"launchBrowser": true,
|
||||
"environmentVariables": {
|
||||
"PERSISTENCE_TYPE": "Sql",
|
||||
"CORS_ORIGINS": "",
|
||||
"PROXIES": "",
|
||||
"QUERY_RATE_PER_MINUTE": "100",
|
||||
"DEFAULT_MESSAGE_LIFETIME_IN_MINUTES": "60",
|
||||
"HOUSEKEEPING_ENABLED": "False",
|
||||
"LOGGING_VERBOSITY": "Trace"
|
||||
},
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5259"
|
||||
}
|
||||
},
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:55327",
|
||||
"sslPort": 44348
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user