update .net, setup modern logging
Some checks failed
Build and Publish / Build Yale Access Backend (pull_request) Failing after 1m28s
Build and Publish / Push Yale Access Backend Docker Image (pull_request) Has been skipped
Build and Publish / Build Yale Access Frontend (pull_request) Successful in 1m42s
Build and Publish / Push Yale Access Frontend Docker Image (pull_request) Has been skipped

This commit is contained in:
2026-02-18 08:48:15 +11:00
parent f577617b4d
commit 6d5749acd3
9 changed files with 84 additions and 122 deletions

View File

@@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
using System.Security.Claims; using System.Security.Claims;
using YaleAccess.Models; using YaleAccess.Models;
@@ -14,15 +13,8 @@ namespace YaleAccess.Controllers
[Route("api/[controller]")] [Route("api/[controller]")]
[EnableCors] [EnableCors]
[Authorize] [Authorize]
public class AuthenticationController : ControllerBase public class AuthenticationController(IOptions<Models.Options.AuthenticationOptions> authenticationOptions, ILogger<AuthenticationController> logger) : ControllerBase
{ {
private readonly Models.Options.AuthenticationOptions _authenticationOptions;
public AuthenticationController(IOptions<Models.Options.AuthenticationOptions> authenticationOptions)
{
_authenticationOptions = authenticationOptions.Value;
}
[HttpPost("login")] [HttpPost("login")]
[AllowAnonymous] [AllowAnonymous]
public async Task<IActionResult> Login([FromBody] string password) public async Task<IActionResult> Login([FromBody] string password)
@@ -30,7 +22,7 @@ namespace YaleAccess.Controllers
try try
{ {
// Check if the password is correct // Check if the password is correct
if (password != _authenticationOptions.Password) if (password != authenticationOptions.Value.Password)
{ {
return Unauthorized(new ApiResponse("Incorrect password.")); return Unauthorized(new ApiResponse("Incorrect password."));
} }
@@ -50,7 +42,7 @@ namespace YaleAccess.Controllers
} }
catch(Exception ex) catch(Exception ex)
{ {
Log.Logger.Error(ex, "An error occurred logging in."); logger.LogError(ex, "An error occurred logging in.");
return BadRequest(new ApiResponse("An error occurred logging in.")); return BadRequest(new ApiResponse("An error occurred logging in."));
} }
} }
@@ -68,7 +60,7 @@ namespace YaleAccess.Controllers
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occured logging out."); logger.LogError(ex, "An error occured logging out.");
return BadRequest(new ApiResponse("An error occured logging out.")); return BadRequest(new ApiResponse("An error occured logging out."));
} }
} }

View File

@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Serilog;
namespace YaleAccess.Controllers namespace YaleAccess.Controllers
{ {
@@ -9,13 +8,13 @@ namespace YaleAccess.Controllers
[Route("api/[controller]")] [Route("api/[controller]")]
[EnableCors] [EnableCors]
[Authorize] [Authorize]
public class HealthController : ControllerBase public class HealthController(ILogger<HealthController> logger) : ControllerBase
{ {
[HttpGet] [HttpGet]
[AllowAnonymous] [AllowAnonymous]
public IActionResult Health() public IActionResult Health()
{ {
Log.Logger.Information("Hit the health endpoint."); logger.LogInformation("Hit the health endpoint.");
return Ok("Service is healthy"); return Ok("Service is healthy");
} }
} }

View File

@@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Serilog;
using YaleAccess.Data; using YaleAccess.Data;
using YaleAccess.Models; using YaleAccess.Models;
@@ -12,27 +11,20 @@ namespace YaleAccess.Controllers
[Route("api/[controller]")] [Route("api/[controller]")]
[EnableCors] [EnableCors]
[Authorize] [Authorize]
public class PeopleController : ControllerBase public class PeopleController(ILogger<PeopleController> logger, YaleContext context) : ControllerBase
{ {
private readonly YaleContext _context;
public PeopleController(YaleContext context)
{
_context = context;
}
[HttpGet] [HttpGet]
public async Task<IActionResult> GetPeople() public async Task<IActionResult> GetPeople()
{ {
try try
{ {
// Return all people // Return all people
List<Person> people = await _context.People.ToListAsync(); List<Person> people = await context.People.ToListAsync();
return Ok(new ApiResponse(people)); return Ok(new ApiResponse(people));
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occured retriving the people."); logger.LogError(ex, "An error occured retriving the people.");
return BadRequest(new ApiResponse("An error occured retriving the people.")); return BadRequest(new ApiResponse("An error occured retriving the people."));
} }
} }
@@ -46,15 +38,15 @@ namespace YaleAccess.Controllers
Person newPerson = new() { Name = person.Name, PhoneNumber = person.PhoneNumber }; Person newPerson = new() { Name = person.Name, PhoneNumber = person.PhoneNumber };
// Add the person // Add the person
await _context.AddAsync(newPerson); await context.AddAsync(newPerson);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
// Return the newly created person // Return the newly created person
return Ok(new ApiResponse(newPerson)); return Ok(new ApiResponse(newPerson));
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occured creating the person."); logger.LogError(ex, "An error occured creating the person.");
return BadRequest(new ApiResponse("An error occured creating the person.")); return BadRequest(new ApiResponse("An error occured creating the person."));
} }
} }
@@ -65,18 +57,18 @@ namespace YaleAccess.Controllers
try try
{ {
// Ensure the person exists // Ensure the person exists
Person person = await _context.People.FindAsync(id) ?? throw new Exception("Person not found."); Person person = await context.People.FindAsync(id) ?? throw new Exception("Person not found.");
// Remove the person // Remove the person
_context.Remove(person); context.Remove(person);
await _context.SaveChangesAsync(); await context.SaveChangesAsync();
// Return the newly removed person // Return the newly removed person
return Ok(new ApiResponse(person)); return Ok(new ApiResponse(person));
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occured deletiong the person."); logger.LogError(ex, "An error occured deletiong the person.");
return BadRequest(new ApiResponse("An error occured deletiong the person.")); return BadRequest(new ApiResponse("An error occured deletiong the person."));
} }
} }

View File

@@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
using YaleAccess.Models; using YaleAccess.Models;
using YaleAccess.Models.Options; using YaleAccess.Models.Options;
using YaleAccess.Services; using YaleAccess.Services;
@@ -14,33 +13,27 @@ namespace YaleAccess.Controllers
[Route("api/[controller]")] [Route("api/[controller]")]
[EnableCors] [EnableCors]
[Authorize] [Authorize]
public class YaleController : ControllerBase public class YaleController(
ILogger<YaleController> logger,
IYaleAccessor yaleAccessor,
IOptions<CodesOptions> codeOptions,
SMSService smsService
) : ControllerBase
{ {
private readonly IYaleAccessor _yaleAccessor;
private readonly CodesOptions _codeOptions;
private readonly SMSService _smsService;
public YaleController(IYaleAccessor yaleAccessor, IOptions<CodesOptions> codeOptions, SMSService smsService)
{
_yaleAccessor = yaleAccessor;
_codeOptions = codeOptions.Value;
_smsService = smsService;
}
[HttpGet("codes")] [HttpGet("codes")]
public async Task<IActionResult> GetUserCodes() public async Task<IActionResult> GetUserCodes()
{ {
try try
{ {
// Get the home code first // Get the home code first
YaleUserCode homeCode = await _yaleAccessor.GetCodeInformationAsync(_codeOptions.Home); YaleUserCode homeCode = await yaleAccessor.GetCodeInformationAsync(codeOptions.Value.Home);
homeCode.IsHome = true; homeCode.IsHome = true;
// Get the guest codes // Get the guest codes
List<YaleUserCode> guestCodes = new(); List<YaleUserCode> guestCodes = new();
foreach (int code in Enumerable.Range(_codeOptions.GuestCodeRangeStart, _codeOptions.GuestCodeRangeCount)) foreach (int code in Enumerable.Range(codeOptions.Value.GuestCodeRangeStart, codeOptions.Value.GuestCodeRangeCount))
{ {
guestCodes.Add(await _yaleAccessor.GetCodeInformationAsync(code)); guestCodes.Add(await yaleAccessor.GetCodeInformationAsync(code));
} }
// Add the home code to the list // Add the home code to the list
@@ -51,7 +44,7 @@ namespace YaleAccess.Controllers
} }
catch(Exception ex) catch(Exception ex)
{ {
Log.Logger.Error(ex, "An error occurred retriving the codes."); logger.LogError(ex, "An error occurred retriving the codes.");
return BadRequest(new ApiResponse("An error occurred retriving the codes.")); return BadRequest(new ApiResponse("An error occurred retriving the codes."));
} }
} }
@@ -69,23 +62,23 @@ namespace YaleAccess.Controllers
} }
// Set the new code // Set the new code
bool result = await _yaleAccessor.SetUserCode(id, newCode); bool result = await yaleAccessor.SetUserCode(id, newCode);
// Return the result // Return the result
if (result) if (result)
{ {
Log.Logger.Information("Updated code for user {id} to {code}", id, newCode); logger.LogInformation("Updated code for user {id} to {code}", id, newCode);
return Ok(new ApiResponse(true)); return Ok(new ApiResponse(true));
} }
else else
{ {
Log.Logger.Information("Failed to update code for user {id} to {code}", id, newCode); logger.LogInformation("Failed to update code for user {id} to {code}", id, newCode);
return BadRequest(new ApiResponse("An error occurred setting the code.")); return BadRequest(new ApiResponse("An error occurred setting the code."));
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occurred setting the code."); logger.LogError(ex, "An error occurred setting the code.");
return BadRequest(new ApiResponse("An error occurred setting the code.")); return BadRequest(new ApiResponse("An error occurred setting the code."));
} }
} }
@@ -96,30 +89,30 @@ namespace YaleAccess.Controllers
try try
{ {
// First validate the user code // First validate the user code
string validCode = YaleAccessor.ValidateClearCode(id, _codeOptions.Home); string validCode = YaleAccessor.ValidateClearCode(id, codeOptions.Value.Home);
if (validCode != string.Empty) if (validCode != string.Empty)
{ {
return BadRequest(new ApiResponse(validCode)); return BadRequest(new ApiResponse(validCode));
} }
// Set the available status // Set the available status
bool result = await _yaleAccessor.SetCodeAsAvailable(id); bool result = await yaleAccessor.SetCodeAsAvailable(id);
// Return the result // Return the result
if (result) if (result)
{ {
Log.Logger.Information("Updated code status for user {id} to available", id); logger.LogInformation("Updated code status for user {id} to available", id);
return Ok(new ApiResponse(true)); return Ok(new ApiResponse(true));
} }
else else
{ {
Log.Logger.Information("Failed to update code status for user {id} to available", id); logger.LogInformation("Failed to update code status for user {id} to available", id);
return BadRequest(new ApiResponse("An error occurred setting the code status.")); return BadRequest(new ApiResponse("An error occurred setting the code status."));
} }
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occurred setting the code status."); logger.LogError(ex, "An error occurred setting the code status.");
return BadRequest(new ApiResponse("An error occurred setting the code status.")); return BadRequest(new ApiResponse("An error occurred setting the code status."));
} }
} }
@@ -130,17 +123,17 @@ namespace YaleAccess.Controllers
try try
{ {
// Get the user code // Get the user code
YaleUserCode userCode = await _yaleAccessor.GetCodeInformationAsync(id); YaleUserCode userCode = await yaleAccessor.GetCodeInformationAsync(id);
// Send the code via SMS to the phone number // Send the code via SMS to the phone number
await _smsService.SendCodeViaSMSAsync(userCode.Code, phoneNumber); await smsService.SendCodeViaSMSAsync(userCode.Code, phoneNumber);
// Return success // Return success
return Ok(new ApiResponse(true)); return Ok(new ApiResponse(true));
} }
catch (Exception ex) catch (Exception ex)
{ {
Log.Logger.Error(ex, "An error occurred sending the code."); logger.LogError(ex, "An error occurred sending the code.");
return BadRequest(new ApiResponse("An error occurred sending the code.")); return BadRequest(new ApiResponse("An error occurred sending the code."));
} }
} }

View File

@@ -1,8 +1,8 @@
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base
WORKDIR /app WORKDIR /app
EXPOSE 80 EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src WORKDIR /src
COPY ["YaleAccess.csproj", "."] COPY ["YaleAccess.csproj", "."]
RUN dotnet restore "./YaleAccess.csproj" RUN dotnet restore "./YaleAccess.csproj"

View File

@@ -1,8 +1,7 @@
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Serilog; using OpenTelemetry.Logs;
using Serilog.Events;
using YaleAccess.Data; using YaleAccess.Data;
using YaleAccess.Models.Options; using YaleAccess.Models.Options;
using YaleAccess.Services; using YaleAccess.Services;
@@ -10,15 +9,25 @@ using YaleAccess.Services.Interfaces;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Create the bootstraper logger
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console()
.CreateLogger();
try try
{ {
var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
builder.Logging.AddOpenTelemetry(logging =>
{
logging.IncludeFormattedMessage = true;
logging.IncludeScopes = true;
if (useOtlpExporter)
{
logging.AddOtlpExporter();
}
else
{
Console.WriteLine("OTEL_EXPORTER_OTLP_ENDPOINT is not set. Skipping OTLP exporter configuration.");
}
});
// Add services to the container. // Add services to the container.
builder.Services.AddControllers(); builder.Services.AddControllers();
@@ -41,13 +50,6 @@ try
// Get a copy of the configuration // Get a copy of the configuration
IConfiguration configuration = builder.Configuration; IConfiguration configuration = builder.Configuration;
string logLocation = configuration["LogLocation"] ?? "Log.txt";
// Setup the application logger
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Error)
.WriteTo.File(logLocation, rollingInterval: RollingInterval.Day)
.CreateLogger();
// Configure the DI services // Configure the DI services
builder.Services.AddScoped<SMSService>(); builder.Services.AddScoped<SMSService>();
@@ -100,9 +102,6 @@ try
}; };
}); });
// Setup logging flow
builder.Host.UseSerilog();
var app = builder.Build(); var app = builder.Build();
// Create the database if it doesn't exist // Create the database if it doesn't exist
@@ -144,11 +143,7 @@ catch (Exception ex)
// Ignore host aborted exceptions caused by build checks // Ignore host aborted exceptions caused by build checks
if (ex is not HostAbortedException) if (ex is not HostAbortedException)
{ {
Log.Fatal(ex, "Host terminated unexpectedly"); Console.WriteLine("Host terminated unexpectedly");
throw; throw;
} }
} }
finally
{
Log.CloseAndFlush();
}

View File

@@ -1,35 +1,27 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
using Twilio; using Twilio;
using Twilio.Rest.Api.V2010.Account; using Twilio.Rest.Api.V2010.Account;
using YaleAccess.Models.Options; using YaleAccess.Models.Options;
namespace YaleAccess.Services namespace YaleAccess.Services
{ {
public class SMSService public class SMSService(ILogger<SMSService> logger, IOptions<TwiloOptions> twiloOptions)
{ {
private readonly TwiloOptions _twiloOptions;
public SMSService(IOptions<TwiloOptions> twiloOptions)
{
_twiloOptions = twiloOptions.Value;
}
public async Task SendCodeViaSMSAsync(string code, string phoneNumber) public async Task SendCodeViaSMSAsync(string code, string phoneNumber)
{ {
// Create a Twilio client // Create a Twilio client
TwilioClient.Init(_twiloOptions.AccountSid, _twiloOptions.AuthToken); TwilioClient.Init(twiloOptions.Value.AccountSid, twiloOptions.Value.AuthToken);
// Send the message // Send the message
var message = await MessageResource.CreateAsync( var message = await MessageResource.CreateAsync(
body: $"{_twiloOptions.Message} {code}", body: $"{twiloOptions.Value.Message} {code}",
from: new Twilio.Types.PhoneNumber(_twiloOptions.FromNumber), from: new Twilio.Types.PhoneNumber(twiloOptions.Value.FromNumber),
to: new Twilio.Types.PhoneNumber(phoneNumber) to: new Twilio.Types.PhoneNumber(phoneNumber)
); );
// Log the message // Log the message
Log.Logger.Information("SMS sent to {PhoneNumber} with message SID {MessageSid}.", phoneNumber, message.Sid); logger.LogInformation("SMS sent to {PhoneNumber} with message SID {MessageSid}.", phoneNumber, message.Sid);
Log.Logger.Debug("SMS message: {MessageBody}", message.Body); logger.LogDebug("SMS message: {MessageBody}", message.Body);
} }
} }
} }

View File

@@ -1,6 +1,5 @@
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Serilog;
using YaleAccess.Models; using YaleAccess.Models;
using YaleAccess.Models.Options; using YaleAccess.Models.Options;
using YaleAccess.Services.Interfaces; using YaleAccess.Services.Interfaces;
@@ -38,10 +37,11 @@ namespace YaleAccess.Services
#endregion Dispose Logic #endregion Dispose Logic
private readonly ILogger<YaleAccessor> _logger;
private Driver? driver = null; private Driver? driver = null;
private readonly ZWaveNode lockNode = null!; private readonly ZWaveNode lockNode = null!;
public YaleAccessor(IOptions<ZWaveOptions> zwave, IOptions<DevicesOptions> device) public YaleAccessor(IOptions<ZWaveOptions> zwave, IOptions<DevicesOptions> device, ILogger<YaleAccessor> logger)
{ {
// Retrive options from configuration // Retrive options from configuration
ZWaveOptions zwaveOptions = zwave.Value; ZWaveOptions zwaveOptions = zwave.Value;
@@ -75,6 +75,8 @@ namespace YaleAccess.Services
// Get the lock node from the driver // Get the lock node from the driver
lockNode = driver.Controller.Nodes.Get(devicesOptions.YaleLockNodeId); lockNode = driver.Controller.Nodes.Get(devicesOptions.YaleLockNodeId);
_logger = logger;
} }
public async Task<YaleUserCode> GetCodeInformationAsync(int userCodeId) public async Task<YaleUserCode> GetCodeInformationAsync(int userCodeId)
@@ -101,7 +103,7 @@ namespace YaleAccess.Services
// If the result is not successful log the message // If the result is not successful log the message
if (!result.Success) if (!result.Success)
{ {
Log.Logger.Error("Failed to set user code {@userCodeId} to {@code}. Error message: {message}", userCodeId, code, result.Message); _logger.LogError("Failed to set user code {@userCodeId} to {@code}. Error message: {message}", userCodeId, code, result.Message);
} }
// Return the result // Return the result
@@ -116,7 +118,7 @@ namespace YaleAccess.Services
// If the result is not successful log the message // If the result is not successful log the message
if (!result.Success) if (!result.Success)
{ {
Log.Logger.Error("Failed to set user code {@userCode} to available status. Error message: {message}", userCode, result.Message); _logger.LogError("Failed to set user code {@userCode} to available status. Error message: {message}", userCode, result.Message);
} }
// Return the result // Return the result

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>83aa9238-2d6b-483c-b60d-886f32d17532</UserSecretsId> <UserSecretsId>83aa9238-2d6b-483c-b60d-886f32d17532</UserSecretsId>
@@ -17,23 +17,20 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.13" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.20" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.20" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.20"> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.3">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.4" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="Semver" Version="2.3.0" /> <PackageReference Include="OpenTelemetry" Version="1.15.0" />
<PackageReference Include="Serilog" Version="3.0.1" /> <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.15.0" />
<PackageReference Include="Serilog.AspNetCore" Version="7.0.0" /> <PackageReference Include="Semver" Version="3.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.3" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="System.Reactive" Version="6.1.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="System.Threading.Channels" Version="5.0.0" />
<PackageReference Include="Twilio" Version="7.8.0" /> <PackageReference Include="Twilio" Version="7.8.0" />
<PackageReference Include="Websocket.Client" Version="4.6.1" /> <PackageReference Include="Websocket.Client" Version="4.6.1" />
</ItemGroup> </ItemGroup>