Git Product home page Git Product logo

back-end's Introduction

back-end

DentallApp is a web application with chatbot for appointment management, reminders and sending appointment cancellation messages for the dental office called World Dental CO.

The maintainer of this repository is Dave Roman.

This project has been improved too much since its first alpha version.

Index

Important

This application was developed as a degree project for the University of Guayaquil, however, it is not ready to run in a production environment. All requirements for this project were obtained through interviews with the owner dentist of World Dental CO.

In the end, this project was never deployed in that dental office for personal reasons of the authors. However, it was decided to publish the source code of this application so that the community can use it for learning purposes (learn from it or even improve it).

Motivations

I have continued to maintain this project because I have been experimenting with plugin-based architecture and I love it.

I have not found any .NET project that has applied this architecture and I don't mean a sample project, but one that solves a problem. For that reason I decided to apply it in this project, I'm sure many will find it useful as knowledge.

Another of my reasons is that what I learn about software engineering, I like to share with the community. That's why I have been inspired to improve it.

The best way to learn things is to do projects.

Technologies used

Softwares

Frameworks and libraries

Testing

Own libraries

Software Engineering

Software engineering concepts have been applied in this project:

Additional references:

Installation

To run this application, it is recommended to install Docker, it is much easier to install the app with Docker.

  • Clone the repository with this command.
git clone https://github.com/DentallApp/back-end.git
  • Change directory.
cd back-end
  • Copy the contents of .env.example to .env.
cp .env.example .env
# On Windows use the "xcopy" command.
  • You must specify the time zone for the Docker container. This is necessary for the calculation of available hours for a medical appointment to return consistent results. The logical thing to do would be to choose the time zone in which the dental clinic is located (which in this case would be America/Guayaquil).
echo -e '\nTZ=America/Guayaquil' >> .env
  • Build the image and initiate services.
docker-compose up --build -d
  • Access the application with this URL.
http://localhost:5000/swagger
  • If you wish to test the chatbot, you can do so with the test client. Access this URL.
https://dentallapp.github.io/webchat-client

NOTE: Twilio.WhatsApp and SendGrid (these are plugins) are not loaded by default. So the app will use a fake provider that uses a console logger (useful for a development environment).

Plugin configuration

By default only two plugins are loaded:

  • Plugin.ChatBot.dll
  • Plugin.AppointmentReminders.dll

You can add other plugins by modifying the PLUGINS key from the .env file:

PLUGINS="
Plugin.ChatBot.dll
Plugin.AppointmentReminders.dll
Plugin.Twilio.WhatsApp.dll
Plugin.SendGrid.dll
"

Of course, for this to work, you will need to create an account on Twilio and SendGrid, generate the necessary credentials and then add it to the .env file.

You can also remove all plugins. The host application will work without any problems.

Credentials

The following table shows the default credentials for authentication from the application.

Username Password
[email protected] 123456
[email protected] 123456
[email protected] 123456
[email protected] 123456
[email protected] 123456

Use this route for authentication:

POST - /api/user/login

Request body:

{
  "userName": "[email protected]",
  "password": "123456"
}

Validate identity documents

To validate identity documents, it depends largely on the country where the dental office is located. At the moment, we can only validate identity documents registered in Ecuador.

You can enable it from the configuration file, e.g.

PLUGINS="
Plugin.IdentityDocument.Ecuador.dll
"

In case there is no plugin loaded to validate the identity document, the host application will use a fake provider called FakeIdentityDocument.

It was decided to implement the logic to validate identity documents from a plugin, because it is flexible, since it allows to change the implementation without having to modify the source code of the host application.

Diagrams

General architecture

Show diagram

general-architecture

More details

Overview of each component:

  • Host Application. Contains everything needed to run the application. It represents the entry point of the application. This layer performs other tasks such as:
    • Load plugins from a configuration file (.env) using the library called CPlugin.Net.
    • Finds the types that implement the interfaces shared between the host application and the plugins to create instances of those types.
    • Add services to the service collection, register middleware, load SQL files, load the .env file, among other things.
  • Shared Layer. It contains common classes and interfaces between many components. This layer has aspects (additional parts) that are not related to the main processes of the application.
    • This layer contains the interfaces that allow communication between the host application and the plugins.
    • This layer does not contain the implementation of a functional requirement.
    • It contains other things such as:
      • Extension classes
      • Exception classes
      • Classes mapped to the database schema (entities)
      • Data models
      • Value objects
      • Objects that represent error and success messages
      • Constants
      • Settings objects
      • Language resources
      • Common validation rules
      • Repository and service interfaces
  • Core Layer. Contains the main processes (essential features) of the application.
    • Each feature represents a functional requirement of what the app should do.
    • A feature contains the minimum code to execute a functional requirement.
    • The purpose of grouping related elements of a feature is to increase cohesion.
    • By convention, each feature module contains a:
      • Controller
      • Request/Response
      • Validator
      • Use case class (has the logic of the functional requirement)
  • Infrastructure Layer. Contains the implementation (concrete classes) of an interface defined in the shared layer.
    • The purpose of this layer is to hide external dependencies that you do not have control over.
    • This layer is useful because it avoids exposing third party dependencies to other components, so if the dependency is changed/removed it should not affect any other component.
    • Not all third party dependencies are added in this layer. For example, Entity Framework Core is used directly in the features to avoid introducing more complexity.
  • ChatBot. It is an plugin that allows a basic user to schedule appointments from a chatbot.
  • Appointment Reminders. It is a plugin that allows to send appointment reminders to patients through a background service.
  • SendGrid Email. It is a plugin that allows to send emails in cases such as:
    • When a customer registers in the application, an email is sent to confirm the user's email address.
    • When a user wants to reset their password, an email is sent with the security token.
  • Twilio WhatsApp. It is a plugin that allows to send messages by whatsapp in cases such as:
    • When an appointment is scheduled from the chatbot, the user is sent the appointment information to whatsapp.
    • When an employee needs to cancel an appointment, he/she should notify patients by whatsapp.
  • IdentityDocument.Ecuador. It is a plugin that allows to validate identity documents registered in Ecuador. This plugin uses an algorithm to verify if the identity document is valid or not.

Core layer

Show diagram

core-layer

More details

The above diagram describes in more detail which feature modules are contained in the core layer.

In the presented diagram it can be identified that the feature modules are not coupled to each other, the purpose of this is not to cause a dependency hell, in order to maintain a dependency graph that is as simple as possible. The purpose is to make it easier to understand the parts of the backend application.

Relational model

Show diagram

relational-model

Direct Line API

Direct Line API allows your client application to communicate with the bot. It acts as a bridge between the client and the bot.

For development and test environments you can use InDirectLine to avoid having to use Azure. InDirectLine is a bridge that implements the Direct Line API, but should not be used for production.

By default, the configuration file (.env) contains a key called DIRECT_LINE_BASE_URL.

DIRECT_LINE_BASE_URL=http://indirectline:3000/

The provider called InDirectLine is used by default.

In production, the value of this key must be changed to:

DIRECT_LINE_BASE_URL=https://directline.botframework.com/

In that case the provider to use will be the Direct Line channel of Azure Bot. The backend application is able to switch providers just by reading the URL.

EF Core Migrations

You can use EF Core migrations to create the database from the entities.

  • You must install dotnet ef as a global tool using the following command:
dotnet tool install --global dotnet-ef
  • Change directory.
cd src/HostApplication
  • Run this command to create the migration files.
dotnet ef migrations add InitialCreate
  • At this point you can have EF create your database and create your schema from the migration.
dotnet ef database update

That's all there is to it - your application is ready to run on your new database, and you didn't need to write a single line of SQL. Note that this way of applying migrations is ideal for local development, but is less suitable for production environments - see the Applying Migrations page for more info.

Running tests

To run the unit tests on your local machine, run this command:

dotnet test ./tests/UnitTests/DentallApp.UnitTests.csproj -c Release

You can also run the chatbot tests on their local machine:

dotnet test ./tests/ChatBot/Plugin.ChatBot.IntegrationTests.csproj -c Release

You can run the integration tests that depend on a database but first you need to follow the following steps:

  • Install MariaDb Server and set up your username and password.
  • Create a file called .env in the root directory with the command:
cp .env.example .env
# On Windows use the "xcopy" command.
  • Create a file called .env.test in the test directory with the command:
cp ./tests/IntegrationTests/.env.test.example ./tests/IntegrationTests/.env.test
# On Windows use the "xcopy" command.
  • Specify your database credentials in the .env.test file.
  • Execute the dotnet test command to run the tests.
dotnet test ./tests/IntegrationTests/DentallApp.IntegrationTests.csproj -c Release

The database credentials you have in the ".env" file may not necessarily be the same as those in the ".env.test" file. For example, the ".env" file may have credentials from a remote AWS database and run the application on your local machine with that connection string.

back-end's People

Contributors

guiller1999 avatar mrdave1999 avatar ts-pytham avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

back-end's Issues

Add a persistence mechanism for jobs

This mechanism is necessary because if the reminders are scheduled to be sent on the first day of each month at 8:00 a.m. but if the server shuts down for several hours before the scheduled time, then the reminder will not be sent when the server starts because it is after 8 a.m.

See Job Stores.

Add project documentation

  • Translate comments from Spanish to English.
  • Add XML comments to controller actions to include it in swagger doc.
  • Produce the type of HTTP status code returned by each controller action.

Avoid injecting an unnecessary dependency in the `Reports` module

Sample code:

public ReportQuery(AppDbContext context, IDbConnector dbConnector)
{
_context = context;
_dbConnector = dbConnector;
}

This class uses both an ORM and raw sql. The problem lies when an operation is executed using raw sql together with Dapper, so the DI container injects an instance of AppDbContext in the constructor of the ReportQuery class, this leads to a waste of memory (because the AppDbContext is never used).
In this case, it would be useful to use the MediatR library and create a per-request handler so that each one uses an AppDbContext or raw sql with Dapper in isolation.

One important detail is that AppDbContext is registered as scoped, so an instance is created in each HTTP request, while IDbConnector is registered as singleton, so it is the same instance in each request.

Create a custom endpoint for generate a Direct Line token

It is necessary to create an endpoint that is responsible for calling the DirectLine API to generate a token and this is because if it is done from the client, the secret key would be exposed, this brings as a consequence that any malicious user could access any conversation associated with the bot.

The solution is to create a service on the back-end side in which it is in charge of making a request to the next endpoint:

POST https://directline.botframework.com/v3/directline/tokens/generate
Authorization: Bearer SECRET_KEY

This can be done on the front-end side but it would be insecure.

Prevent the user from not being able to use the chatbot if not authenticated

Currently the endpoint where the bot listens is public, so anyone could communicate with the bot. It would be nice if only authenticated users could access the bot from the website.

In addition, the same security token can be passed to the bot as the one provide by the website when the user logs in.
In that case, on the bot side, the access token would have to be validated to check if the user has authenticated.

If no check is performed, any user can schedule appointments for other users simply by knowing the user ID (this is known as user impersonation).

Avoid coupling classes with `DateTime.Now`

Currently the IDateTimeProvider interface (which is in the Helpers module) has been added for this problem, but some modules still need to be refactored to depend on the interface to facilitate unit testing.

When using the `dotnet ef` command displays warnings related to global query filters

The CLI displays the following warnings:

warn: Microsoft.EntityFrameworkCore.Model.Validation[10622]
      Entity 'Employee' has a global query filter defined and is the required end of a relationship with the entity 'Appoinment'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.

warn: Microsoft.EntityFrameworkCore.Model.Validation[10622]
      Entity 'Employee' has a global query filter defined and is the required end of a relationship with the entity 'EmployeeSchedule'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.

warn: Microsoft.EntityFrameworkCore.Model.Validation[10622]
      Entity 'Employee' has a global query filter defined and is the required end of a relationship with the entity 'FavoriteDentist'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.

warn: Microsoft.EntityFrameworkCore.Model.Validation[10622]
      Entity 'GeneralTreatment' has a global query filter defined and is the required end of a relationship with the entity 'SpecificTreatment'. This may lead to unexpected results when the required entity is filtered out. Either configure the navigation as optional, or define matching query filters for both entities in the navigation. See https://go.microsoft.com/fwlink/?linkid=2131316 for more information.

Could these warnings impair the application or bring unexpected results?

Add each feature in a file to represent the vertical slice

Example:

In the Dependents module there would be four files representing the vertical slice:

  • CreateDependent.cs
  • DeleteDependent.cs
  • UpdateDependent.cs
  • GetDependentsByUserId.cs

Each file represents a feature and in it are the related components such as the use case, mapper, validator and the DTOs (request/response).

Reduce code in POST/PUT operations of controllers

SAMPLES

The following codes can be simplified by using the ternary operator:

public async Task<ActionResult<Response<DtoBase>>> Post([FromBody]AppointmentInsertDto appointmentInsertDto)
{
var response = await _appointmentService.CreateAppointmentAsync(appointmentInsertDto);
if (response.Success)
return CreatedAtAction(nameof(Post), response);
return BadRequest(response);
}

public async Task<ActionResult<Response>> Put(int id, [FromBody]AppointmentUpdateDto appointmentUpdateDto)
{
var response = await _appointmentService.UpdateAppointmentAsync(id, User, appointmentUpdateDto);
if (response.Success)
return Ok(response);
return BadRequest(response);
}

Remove abstractions that represent application logic

The abstraction is only used once and has only one implementation associated with it. This only adds unnecessary complexity to the project and complicates maintenance, since changes must be made to both the abstraction and the concrete class when a new method needs to be added.

Load bot credentials from an .env file

The following keys must be set up from an .env:

  • MICROSOFT_APP_TYPE=
  • MICROSOFT_APP_ID=
  • MICROSOFT_APP_PASSWORD=
  • MICROSOFT_APP_TENANT_ID=

The default value of each key must be an empty string.

Unit tests take more than a minute to run

Something is happening but unit tests are taking a long time to run. I'm not sure if it's the simulation tests that are performed between the bot and the user, as these tests are more of integration, because they depend on the file system.

Implement each layer in a separate project

Purpose

The purpose of separating these layers in your own project is to ensure that the interdependence between them is not broken.

For example, the shared layer does not need to depend on any other layer. It is a simple "mediator" that allows communication between the layers, thus avoiding coupling between them.
In case I accidentally add a class from another layer in the shared layer, the compiler will generate a compilation error that the type does not exist or there is no reference to another project. This avoids accidentally coupling one layer with another.

The current diagram represents a part of dentallapp's architecture:

backend-architecture

The classes should not be coupled with data provider

For example, this class is coupled to MySqlConnection and should actually depend on an abstraction to facilitate dependency inversion:

public async Task<ReportGetTotalAppoinmentDto> GetTotalAppoinmentsByDateRangeAsync(ReportPostWithDentistDto reportPostDto)
{
using var connection = new MySqlConnection(_settings.ConnectionString);
var sql = @"
SELECT

Also, in this case, the using global directive could be used to avoid having to import the types in each file:

๏ปฟusing Dapper;
using MySqlConnector;

IDX10501: Signature validation failed. Unable to match key: Failed to validate the token.

I have deployed this project on a test virtual server and in addition, I created the Azure Bot Service resource to register the bot and attach the DirectLine channel in the resource, however, when interacting with the bot from the WebChat (a component of Bot Framework-WebChat) the following error occurs on the server side:

Jan 12 13:50:15 DentallApp bash[1582]: info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]
Jan 12 13:50:15 DentallApp bash[1582]:       token: '{"alg":"RS256","kid":"d25T3rS8ZCu8VUIqDV3fV14llFI","x5t":"d25T3rS8ZCu8VUIqDV3fV14llFI","typ":"JWT","cty":"JWT"}.{"serviceurl":"https://directline.botframework.com/","nbf":1673549415,"exp":1673550015,"iss":"https://api.botframework.com","aud":"023e5038-8c60-45ee-87a2-852aa984480c"}'.
Jan 12 13:50:15 DentallApp bash[1582]:       '.
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters, BaseConfiguration configuration)
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(Byte[] encodedBytes, Byte[] signature, SecurityKey key, String algorithm, SecurityToken securityToken, TokenValidationParameters validationParameters)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateForVerifying(SecurityKey key, String algorithm)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateForVerifying(SecurityKey key, String algorithm, Boolean cacheProvider)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateSignatureProvider(SecurityKey key, String algorithm, Boolean willCreateSignatures, Boolean cacheProvider)
Jan 12 13:50:15 DentallApp bash[1582]:        is not supported. The list of supported algorithms is available here: https://aka.ms/IdentityModel/supported-algorithms
Jan 12 13:50:15 DentallApp bash[1582]:       Algorithm: 'RS256', SecurityKey: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'd1JZdyHpnBVySxTBXp85dMx1k7Hb2R4DAYUctaaQCtE'.'
Jan 12 13:50:15 DentallApp bash[1582]:        'System.NotSupportedException: IDX10634: Unable to create the SignatureProvider.
Jan 12 13:50:15 DentallApp bash[1582]:       Exceptions caught:
Jan 12 13:50:15 DentallApp bash[1582]:       Number of keys in Configuration: '1'.
Jan 12 13:50:15 DentallApp bash[1582]:       Number of keys in TokenValidationParameters: '0'.
Jan 12 13:50:15 DentallApp bash[1582]:       kid: 'd25T3rS8ZCu8VUIqDV3fV14llFI'.
Jan 12 13:50:15 DentallApp bash[1582]:       Bearer was not authenticated. Failure message: IDX10501: Signature validation failed. Unable to match key:
Jan 12 13:50:15 DentallApp bash[1582]: info: Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler[7]
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, TokenValidationParameters validationParameters, SecurityToken& validatedToken)
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateToken(String token, JwtSecurityToken outerToken, TokenValidationParameters validationParameters, SecurityToken& signatureValidatedToken)
Jan 12 13:50:15 DentallApp bash[1582]:       --- End of stack trace from previous location ---
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateJWS(String token, TokenValidationParameters validationParameters, BaseConfiguration currentConfiguration, SecurityToken& signatureValidatedToken, ExceptionDispatchInfo& exceptionThrown)
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters, BaseConfiguration configuration)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.InternalValidators.ValidateLifetimeAndIssuerAfterSignatureNotValidatedJwt(SecurityToken securityToken, Nullable`1 notBefore, Nullable`1 expires, String kid, TokenValidationParameters validationParameters, BaseConfiguration configuration, StringBuilder exceptionStrings, Int32 numKeysInConfiguration, Int32 numKeysInTokenValidationParameters)
Jan 12 13:50:15 DentallApp bash[1582]:       token: '{"alg":"RS256","kid":"d25T3rS8ZCu8VUIqDV3fV14llFI","x5t":"d25T3rS8ZCu8VUIqDV3fV14llFI","typ":"JWT","cty":"JWT"}.{"serviceurl":"https://directline.botframework.com/","nbf":1673549415,"exp":1673550015,"iss":"https://api.botframework.com","aud":"023e5038-8c60-45ee-87a2-852aa984480c"}'.
Jan 12 13:50:15 DentallApp bash[1582]:       '.
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(String token, TokenValidationParameters validationParameters, BaseConfiguration configuration)
Jan 12 13:50:15 DentallApp bash[1582]:          at System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler.ValidateSignature(Byte[] encodedBytes, Byte[] signature, SecurityKey key, String algorithm, SecurityToken securityToken, TokenValidationParameters validationParameters)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateForVerifying(SecurityKey key, String algorithm)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateForVerifying(SecurityKey key, String algorithm, Boolean cacheProvider)
Jan 12 13:50:15 DentallApp bash[1582]:          at Microsoft.IdentityModel.Tokens.CryptoProviderFactory.CreateSignatureProvider(SecurityKey key, String algorithm, Boolean willCreateSignatures, Boolean cacheProvider)
Jan 12 13:50:15 DentallApp bash[1582]:        is not supported. The list of supported algorithms is available here: https://aka.ms/IdentityModel/supported-algorithms
Jan 12 13:50:15 DentallApp bash[1582]:       Algorithm: 'RS256', SecurityKey: 'Microsoft.IdentityModel.Tokens.SymmetricSecurityKey, KeyId: '', InternalId: 'd1JZdyHpnBVySxTBXp85dMx1k7Hb2R4DAYUctaaQCtE'.'
Jan 12 13:50:15 DentallApp bash[1582]:        'System.NotSupportedException: IDX10634: Unable to create the SignatureProvider.
Jan 12 13:50:15 DentallApp bash[1582]:       Exceptions caught:
Jan 12 13:50:15 DentallApp bash[1582]:       Number of keys in Configuration: '1'.
Jan 12 13:50:15 DentallApp bash[1582]:       Number of keys in TokenValidationParameters: '0'.
Jan 12 13:50:15 DentallApp bash[1582]:       kid: 'd25T3rS8ZCu8VUIqDV3fV14llFI'.
Jan 12 13:50:15 DentallApp bash[1582]:       Microsoft.IdentityModel.Tokens.SecurityTokenSignatureKeyNotFoundException: IDX10501: Signature validation failed. Unable to match key:
Jan 12 13:50:15 DentallApp bash[1582]:       Failed to validate the token.

This error occurs every time a message is sent to the bot, although the bot still works without any problem.

Remove child layer repositories

Child layer repositories such as Kinship, Gender and Appointment Status only add unnecessary complexity, so they can be eliminated as they are very simple queries.

Add the kinship in the choices

When the chatbot asks the user to select a patient for appointment scheduling, it could be displayed like this:
Image
The kinship is displayed next to the patient's name.

Add Docker support

The following files should be created:

  • Dockerfile
  • docker-compose.yml: It has three services: webapi, database and InDirectLine.

Must also create the InDirectLine image and upload it in DockerHub to use it from docker-compose.yml.

The client must perform the following steps to deploy the back-end:

  • Clone the repository.
  • Configure the .env file.
  • Execute the command: docker compose up --build -d.

Apply the null-condition operator to format dates

This code:

public static string GetDateAndHourInSpanishFormat(this DateTime? dateTimeNullable)
{
if (dateTimeNullable is null)
return null;
DateTime dt = (DateTime)dateTimeNullable;
return dt.ToString("f", new System.Globalization.CultureInfo("es-ES"));
}

Can be converted to:

 public static string GetDateAndHourInSpanishFormat(this DateTime? dt) 
     => dt?.ToString("f", new System.Globalization.CultureInfo("es-ES")); 

The compiler translates the above code to:

public static string GetDateAndHourInSpanishFormat(Nullable<DateTime> dt)
{
     return dt.HasValue ? dt.GetValueOrDefault().ToString("f", new CultureInfo("es-ES")) : null;
}

Send requests to the database once a day to remind appointments

Currently, there is a background process that sends an it hourly request to the database in order to get the appointments that are close to the stipulated date and then send the appointment reminders to the patients. Instead of sending it hourly, the request could be sent in the morning at 8:00 a.m. (this could be modified from a .env file).

PD: So with the above change, the HasReminder property of the Appointment model would no longer be needed (this property was used to ensure that the reminder is sent only once to the patient).

Replace the `IDbConnector` interface by `IDbConnection`

This connector is unnecessary, since you can use the provider class as MySqlConnection:

๏ปฟusing MySqlConnector;
namespace DentallApp.DataAccess.DbConnectors;
public class MariaDbConnector : IDbConnector
{
private readonly string _connectionString;
public MariaDbConnector(string connectionString)
=> _connectionString = connectionString;
public IDbConnection CreateConnection()
=> new MySqlConnection(_connectionString);
}

That abstraction layer is not needed, because IDbConnection can be registered as a service, for example:

builder.Services.AddScoped<IDbConnection>(
                 serviceProvider => new MySqlConnection("MY_CONNECTION_STRING"));

Another advantage of this approach is that it is no longer necessary to use the using statement to release the resource, because the DI container takes care of that responsibility (behind the scenes it invokes the Dispose method).

Remove single-line methods at the service layer

For example:

public async Task<IEnumerable<OfficeGetDto>> GetOfficesAsync()
=> await _officeRepository.GetOfficesAsync();
public async Task<IEnumerable<OfficeShowDto>> GetOfficesForEditAsync()
=> await _officeRepository.GetOfficesForEditAsync();
public async Task<IEnumerable<OfficeGetDto>> GetAllOfficesAsync()
=> await _officeRepository.GetAllOfficesAsync();

These methods are useless, they do not provide any benefit, they only make maintenance more difficult.

Get the list of dentists for the basic user

The following method gets the list of the basic user's favorite dentists and also includes the dentists that are not preferred by the userId:

public async Task<IEnumerable<DentistGetDto>> GetListOfDentistsAsync(int userId)
=> await (from dentist in Context.Set<Employee>()
join dentistDetails in Context.Set<Person>() on dentist.PersonId equals dentistDetails.Id
join office in Context.Set<Office>() on dentist.OfficeId equals office.Id
join userRole in Context.Set<UserRole>() on
new
{
RoleId = RolesId.Dentist,
dentist.UserId
}
equals
new
{
userRole.RoleId,
userRole.UserId
}
join favoriteDentist in Context.Set<FavoriteDentist>() on
new
{
UserId = userId,
dentist.Id
}
equals
new
{
favoriteDentist.UserId,
Id = favoriteDentist.DentistId
} into favoriteDentists
from favoriteDentist in favoriteDentists.DefaultIfEmpty()
select new DentistGetDto
{
DentistId = dentist.Id,
FullName = dentistDetails.FullName,
PregradeUniversity = dentist.PregradeUniversity,
PostgradeUniversity = dentist.PostgradeUniversity,
OfficeId = dentist.OfficeId,
OfficeName = office.Name,
IsFavorite = favoriteDentist != null
}).ToListAsync();

But the curious line of code is the following:

IsFavorite = favoriteDentist != null

The above line EF Core translates it to:

`f`.`id` IS NOT NULL AS `IsFavorite`

f is a simple alias of the favorite_dentists table.

The strangest thing is that if the code is changed to:

IsFavorite            = favoriteDentist.Id != null 

And the result is the same as in the previous case:

`f`.`id` IS NOT NULL AS `IsFavorite`

In both cases the same sql code is generated and returns the expected result, only that in this expression a warning is thrown that an int cannot be compared with null:

// Generates a warning but the LINQ query continues to work.
favoriteDentist.Id != null 

There is an issue that talks about this: dotnet/efcore#22517

So the question is, can our project rely on this behavior?

PD: The final SQL code generated by EF Core is:

SELECT
`e`.`id` AS `DentistId`, 
CONCAT(CONCAT(COALESCE(`p`.`names`, ''), ' '), COALESCE(`p`.`last_names`, '')) AS `FullName`, 
`e`.`pregrade_university` AS `PregradeUniversity`, 
`e`.`postgrade_university` AS `PostgradeUniversity`, 
`e`.`office_id` AS `OfficeId`, 
`t`.`name` AS `OfficeName`, 
`f`.`id` IS NOT NULL AS `IsFavorite`
FROM `employees` AS `e`
INNER JOIN `persons` AS `p` ON `e`.`person_id` = `p`.`id`
INNER JOIN (
   	SELECT `o`.`id`, `o`.`name`
   	FROM `offices` AS `o`
   	WHERE NOT (`o`.`is_deleted`)
   ) AS `t` ON `e`.`office_id` = `t`.`id`
INNER JOIN `user_roles` AS `u` ON (4 = `u`.`role_id`) AND (`e`.`user_id` = `u`.`user_id`)
LEFT JOIN `favorite_dentists` AS `f` ON (@__userId_0 = `f`.`user_id`) AND (`e`.`id` = `f`.`dentist_id`)
WHERE NOT (`e`.`is_deleted`)

Migrate to .net 7.0 and EF Core 7.0

These are the packages to be upgraded to v4.18.1:

<PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.18.1" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.18.1" />

Now with this change, the project can change from target framework to .net 7.0, previously it was not possible because bot framework worked with .netcoreapp 3.1 (see microsoft/botframework-sdk#6551).

AuthenticationScheme: Bearer was not authenticated

When accessing an unprotected endpoint such as http://localhost:3978/api/gender, the following message is displayed:

dbug: DentallApp.Extensions.AuthenticationJwtBearer+CustomJwtBearerHandler[9]
      AuthenticationScheme: Bearer was not authenticated.
info: Microsoft.AspNetCore.Routing.EndpointMiddleware[0]

This should not be displayed because at no point is the Authorization header included in the HTTP request, i.e. the Bearer does not actually provide any access token.

This is strange behavior, as this does not impair the operation of the application. An important detail is that this message is only displayed in development mode.
If we change ASPNETCORE_ENVIRONMENT to Production, the message disappears.

PD: This behavior arose from PR #136.

Remove DbSets from the `AppDbContext` class

In the code the generic Set<T> method is being used, so these properties are unusable and only saturate the context instance:

public DbSet<GeneralTreatment> GeneralTreatments { get; set; }
public DbSet<SpecificTreatment> SpecificTreatments { get; set; }
public DbSet<Person> Persons { get; set; }
public DbSet<Gender> Genders { get; set; }
public DbSet<User> Users { get; set; }
public DbSet<UserRole> UserRoles { get; set; }
public DbSet<Role> Roles { get; set; }
public DbSet<Dependent> Dependents { get; set; }
public DbSet<Kinship> Kinships { get; set; }
public DbSet<Employee> Employees { get; set; }
public DbSet<Office> Offices { get; set; }
public DbSet<Appoinment> Appoinments { get; set; }
public DbSet<AppoinmentStatus> AppoinmentsStatus { get; set; }
public DbSet<EmployeeSchedule> EmployeeSchedules { get; set; }
public DbSet<WeekDay> WeekDays { get; set; }
public DbSet<FavoriteDentist> FavoriteDentists { get; set; }
public DbSet<OfficeSchedule> OfficeSchedules { get; set; }

EF Core select all columns in SQL when a DTO mapper is called on Select()

The following LINQ:

=> await Context.Set<User>()
.Include(user => user.Person)
.Where(user => user.UserName == username)
.Select(user => new UserResetPasswordDto()
{
UserId = user.Id,
UserName = user.UserName,
Name = user.Person.Names,
Password = user.Password
})
.FirstOrDefaultAsync();

EF Core translates it to:

SELECT `u`.`id` AS `UserId`, `u`.`username` AS `UserName`, `p`.`names` AS `Name`, `u`.`password` AS `Password`
FROM `users` AS `u`
INNER JOIN `persons` AS `p` ON `u`.`person_id` = `p`.`id`
WHERE `u`.`username` = @__username_0
LIMIT 1

In this case EF Core selects the columns associated to UserResetPasswordDto.

But when the mapper is called in the Select method:

.Select(user => user.MapToUserResetPasswordDto()) 

EF Core translates it to:

SELECT `u`.`id`, `u`.`created_at`, `u`.`password`, `u`.`person_id`, `u`.`refresh_token`, `u`.`refresh_token_expiry`, `u`.`updated_at`, `u`.`username`, `p`.`id`, `p`.`cell_phone`, `p`.`created_at`, `p`.`date_birth`, `p`.`document`, `p`.`email`, `p`.`gender_id`, `p`.`last_names`, `p`.`names`, `p`.`updated_at`
FROM `users` AS `u`
INNER JOIN `persons` AS `p` ON `u`.`person_id` = `p`.`id`
WHERE `u`.`username` = @__username_0
LIMIT 1

EF Core selects ALL columns. The mapper code is:

public static UserResetPasswordDto MapToUserResetPasswordDto(this User user)
=> new()
{
UserId = user.Id,
UserName = user.UserName,
Name = user.Person.Names,
Password = user.Password
};

There is a possible solution in this StackOverflow answer: https://stackoverflow.com/a/62138200 (must be tested)

This problem is also mentioned here: dotnet/efcore#24509

Images of dental services cannot have white-spaces

On Windows there are no problems but on Linux distributions when a file name has whitespace, it is appended around single quotes. This leads to invalid behavior, because in the database the image name is stored without single quotes but in the file system it is (this should not be the case).

This would cause problems, for example, when you want to edit the image of a dental service, it will not be possible to delete the old image because the image name in the database will not match the image in the file system.

Sample:

File system:

'brackets_0023222.png'

database:

brackets_0023222.png

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.