Initial Sync.

This commit is contained in:
2023-12-07 14:33:15 +01:00
parent f2640148d3
commit 3b83fcf7b9
49 changed files with 2822 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
+32
View File
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>72602978-2f98-4a25-bd8b-1f2274a49fe4</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
<RootNamespace>DGM.HoC.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="Protos\BookMng.proto" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\BookMng.proto" GrpcServices="Server" />
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Grpc.AspNetCore" Version="2.53.0" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.53.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.18.1" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
</ItemGroup>
</Project>
+22
View File
@@ -0,0 +1,22 @@
#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:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["BookMng.API/BookMng.API.csproj", "BookMng.API/"]
RUN dotnet restore "BookMng.API/BookMng.API.csproj"
COPY . .
WORKDIR "/src/BookMng.API"
RUN dotnet build "BookMng.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "BookMng.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "BookMng.API.dll"]
@@ -0,0 +1,28 @@
using DGM.HoC.BookMng.API.Infrastructure.EntityConfigs;
using DGM.HoC.BookMng.API.Model;
using Microsoft.EntityFrameworkCore;
namespace DGM.HoC.BookMng.API.Infrastructure;
public class BookingContext : DbContext
{
public BookingContext(DbContextOptions<BookingContext> options)
: base(options)
{ }
public DbSet<Booking> Bookings { get; set; }
public DbSet<RoomType> RoomTypes { get; set; }
public DbSet<RoomItem> RoomItems { get; set; }
public DbSet<CustomerData> BookingCustomerData { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new BookingTypeEntityConfig());
modelBuilder.ApplyConfiguration(new RoomItemTypeEntityConfig());
modelBuilder.ApplyConfiguration(new CustomerDataTypeEntityConfig());
base.OnModelCreating(modelBuilder);
}
}
@@ -0,0 +1,117 @@
namespace DGM.HoC.BookMng.API.Infrastructure
{
public class BookingContextSeed
{
public static void InitData(BookingContext dbContext)
{
if (dbContext.RoomTypes.Count() == 0)
{
InitRoomTypes(dbContext);
}
if (dbContext.Bookings.Count() == 0)
{
InitBookings(dbContext);
}
}
public static void InitRoomTypes(BookingContext dbContext)
{
dbContext.RoomTypes.Add(new Model.RoomType()
{
Id = 1,
Name = "Individual con baño",
Description = "Individual con baño privado, televisión y wifi",
GuestAmount = 1
});
dbContext.RoomTypes.Add(new Model.RoomType()
{
Id = 2,
Name = "Matrimonio con baño",
Description = "Doble de matrimonio, con baño privado, televisión y wifi",
GuestAmount = 2
});
dbContext.RoomTypes.Add(new Model.RoomType()
{
Id = 3,
Name = "Dos camas con baño",
Description = "Doble de dos camas, con baño privado, televisión y wifi",
GuestAmount = 2
});
dbContext.RoomTypes.Add(new Model.RoomType()
{
Id = 4,
Name = "Triple con vistas",
Description = "Habitación triple, con una cama matrimonio y otra individual, mirador con vistas, baño privado, televisión y wifi",
GuestAmount = 3
});
dbContext.SaveChanges();
}
public static void InitBookings(BookingContext dbContext)
{
dbContext.Bookings.Add(new Model.Booking()
{
Id = 1,
CustomerId = "N12345",
Status = "Reservada",
Description = "Vienen de paso, probablemente con caballos y carga"
});
dbContext.RoomItems.Add(new Model.RoomItem()
{
BookingId = 1,
Id = 1,
TypeId = 3,
GuestAmount = 2,
DateIn = new DateTime(2023, 5, 23),
DateOut = new DateTime(2023, 5, 24),
PricePerNight = 45,
Notes = ""
});
dbContext.RoomItems.Add(new Model.RoomItem()
{
BookingId = 1,
Id = 2,
TypeId = 1,
GuestAmount = 1,
DateIn = new DateTime(2023, 5, 23),
DateOut = new DateTime(2023, 5, 24),
PricePerNight = 30,
Notes = ""
});
dbContext.Bookings.Add(new Model.Booking()
{
Id = 2,
CustomerId = "N567432",
Status = "Finalizada",
Description = "Gandalf llamó para reservar para un día, puede que se quede alguno más."
});
dbContext.RoomItems.Add(new Model.RoomItem()
{
BookingId = 2,
Id = 1,
TypeId = 1,
GuestAmount = 1,
DateIn = new DateTime(2023, 4, 10),
DateOut = new DateTime(2023, 5, 11),
PricePerNight = 25,
Notes = ""
});
dbContext.SaveChanges();
}
}
}
@@ -0,0 +1,20 @@
using DGM.HoC.BookMng.API.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DGM.HoC.BookMng.API.Infrastructure.EntityConfigs;
public class BookingTypeEntityConfig : IEntityTypeConfiguration<Booking>
{
public void Configure(EntityTypeBuilder<Booking> builder)
{
builder.HasMany(b => b.Rooms)
.WithOne()
.HasForeignKey(r => r.BookingId)
.IsRequired();
builder.HasOne(b => b.BookingCustomer)
.WithOne()
.HasForeignKey<CustomerData>(cd => cd.BookingId);
}
}
@@ -0,0 +1,13 @@
using DGM.HoC.BookMng.API.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DGM.HoC.BookMng.API.Infrastructure.EntityConfigs;
public class CustomerDataTypeEntityConfig : IEntityTypeConfiguration<CustomerData>
{
public void Configure(EntityTypeBuilder<CustomerData> builder)
{
builder.HasKey(cd => cd.BookingId);
}
}
@@ -0,0 +1,18 @@
using DGM.HoC.BookMng.API.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DGM.HoC.BookMng.API.Infrastructure.EntityConfigs;
public class RoomItemTypeEntityConfig : IEntityTypeConfiguration<RoomItem>
{
public void Configure(EntityTypeBuilder<RoomItem> builder)
{
builder.HasKey(item => new { item.BookingId, item.Id });
builder.HasOne(item => item.RoomType)
.WithMany()
.HasForeignKey(item => item.TypeId)
.IsRequired();
}
}
@@ -0,0 +1,36 @@
using AutoMapper;
using DGM.HoC.BookMng.API.Model;
using DGM.HoC.BookMng.API.RPC;
using Google.Protobuf.Collections;
namespace DGM.HoC.BookMng.API.Infrastructure.Mappers;
public class EnumerableRoomItemToRepeatedFieldRpcRoomItem : IValueConverter<IEnumerable<RoomItem>, RepeatedField<rpcRoomItem>>
{
public RepeatedField<rpcRoomItem> Convert(IEnumerable<RoomItem> sourceMember, ResolutionContext context)
{
RepeatedField<rpcRoomItem> rpf = new RepeatedField<rpcRoomItem>();
foreach(var room in sourceMember)
{
rpf.Add(context.Mapper.Map<RoomItem, rpcRoomItem>(room));
}
return rpf;
}
}
public class RepeatedFieldRpcRoomItemToEnumerableRoomItem : IValueConverter<RepeatedField<rpcRoomItem>, IEnumerable<RoomItem>>
{
public IEnumerable<RoomItem> Convert(RepeatedField<rpcRoomItem> sourceMember, ResolutionContext context)
{
List<RoomItem> roomList = new List<RoomItem>();
foreach(var room in sourceMember)
{
var rpc = context.Mapper.Map<rpcRoomItem, RoomItem>(room);
roomList.Add(rpc);
}
return roomList;
}
}
@@ -0,0 +1,45 @@
using AutoMapper;
using DGM.HoC.BookMng.API.Model;
using DGM.HoC.BookMng.API.RPC;
using Google.Protobuf.Collections;
using Microsoft.AspNetCore.Routing.Constraints;
namespace DGM.HoC.BookMng.API.Infrastructure.Mappers;
public class MappingProfile : Profile
{
public MappingProfile()
{
CreateMap<RoomType, rpcRoomType>();
CreateMap<rpcRoomType, RoomType>();
CreateMap<CustomerData, rpcBookingCustomer>();
CreateMap<rpcBookingCustomer, CustomerData>();
//TODO: Create mapping from String to DateTime.
CreateMap<string, DateTime>().ConvertUsing<StringToDateTimeConverter>();
CreateMap<DateTime, string>().ConvertUsing<DateTimeToStringConverter>();
CreateMap<RoomItem, rpcRoomItem>(MemberList.Destination)
.ForMember(dest => dest.ItemId, opt => opt.MapFrom(src => src.Id));
CreateMap<rpcRoomItem, RoomItem>(MemberList.Source)
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.ItemId));
CreateMap<Booking, rpcBooking>(MemberList.Destination);
CreateMap<rpcBooking, Booking>(MemberList.Source);
//TODO: Crear un Custom Type Converter: https://docs.automapper.org/en/stable/Custom-type-converters.html
CreateMap<Booking, rpcBookingDetails>()
.ForPath(dest => dest.Data.Id, opt => opt.MapFrom(src => src.Id))
.ForPath(dest => dest.Data.Description, opt => opt.MapFrom(src => src.Description))
.ForPath(dest => dest.Data.Status, opt => opt.MapFrom(src => src.Status))
.ForPath(dest => dest.Data.CustomerId, opt => opt.MapFrom(src => src.CustomerId))
.ForMember(dest => dest.Rooms, opt => opt.ConvertUsing<EnumerableRoomItemToRepeatedFieldRpcRoomItem, IEnumerable<RoomItem>>());
CreateMap<rpcBookingDetails, Booking>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Data.Id))
.ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Data.Description))
.ForMember(dest => dest.Status, opt => opt.MapFrom(src => src.Data.Status))
.ForMember(dest => dest.CustomerId, opt => opt.MapFrom(src => src.Data.CustomerId))
.ForMember(dest => dest.Rooms, opt => opt.ConvertUsing<RepeatedFieldRpcRoomItemToEnumerableRoomItem, RepeatedField<rpcRoomItem>>());
}
}
@@ -0,0 +1,29 @@
using DGM.HoC.BookMng.API.Model;
using DGM.HoC.BookMng.API.RPC;
namespace DGM.HoC.BookMng.API.Infrastructure.Mappers;
public class RoomTypeMapper
{
public static RoomType MapRpcRoomTypeToRoomType(rpcRoomType rpc)
{
return new RoomType
{
Id = rpc.Id,
Name = rpc.Name,
Description = rpc.Description,
GuestAmount = rpc.GuestAmount
};
}
public static rpcRoomType MapRoomTypeToRpcRoomType(RoomType type)
{
return new rpcRoomType
{
Id = type.Id,
Name = type.Name,
Description = type.Description,
GuestAmount = type.GuestAmount
};
}
}
@@ -0,0 +1,32 @@
using AutoMapper;
using System.Globalization;
namespace DGM.HoC.BookMng.API.Infrastructure.Mappers
{
public class StringToDateTimeConverter : ITypeConverter<string, DateTime>
{
public DateTime Convert(string source, DateTime destination, ResolutionContext context)
{
var esES = new CultureInfo("es-ES");
DateTime dateTime;
if (DateTime.TryParseExact(source, "dd/MM/yyyy", esES, DateTimeStyles.AllowLeadingWhite, out dateTime))
{
return dateTime;
}
return DateTime.UnixEpoch;
}
}
public class DateTimeToStringConverter : ITypeConverter<DateTime, string>
{
public string Convert(DateTime source, string destination, ResolutionContext context)
{
return source.ToString();
}
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace DGM.HoC.BookMng.API.Model;
public class Booking
{
public int Id { get; set; }
public string Description { get; set; }
public string Status { get; set; }
public IEnumerable<RoomItem> Rooms { get; set; }
public CustomerData? BookingCustomer { get; set; }
public string? CustomerId { get; set; }
}
+10
View File
@@ -0,0 +1,10 @@
namespace DGM.HoC.BookMng.API.Model;
public class CustomerData
{
public int BookingId { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
}
+19
View File
@@ -0,0 +1,19 @@
namespace DGM.HoC.BookMng.API.Model;
public class RoomItem
{
//Primary key (BookingId, Id).
public int BookingId { get; set; }
public int Id { get; set; }
public int TypeId { get; set; }
public int GuestAmount { get; set; }
public DateTime DateIn { get; set; }
public DateTime DateOut { get; set; }
public float PricePerNight { get; set; }
public string Notes { get; set; }
public RoomType RoomType { get; set; }
}
+11
View File
@@ -0,0 +1,11 @@
namespace DGM.HoC.BookMng.API.Model;
public class RoomType
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int GuestAmount { get; set; }
}
+91
View File
@@ -0,0 +1,91 @@
using AutoMapper;
using BookMng.API.Services;
using DGM.HoC.BookMng.API.Infrastructure;
using DGM.HoC.BookMng.API.Infrastructure.Mappers;
using DGM.HoC.BookMng.API.Services;
using Microsoft.EntityFrameworkCore;
namespace BookMng.API
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();
builder.Services.AddDbContext<BookingContext>(options =>
{
string? connString = builder.Environment.IsDevelopment() ? builder.Configuration["HOC:BOOKMNG:DB"] : builder.Configuration["HOC_DBString"];
options.UseMySql(connString, new MariaDbServerVersion(new Version(10, 11, 2)));
});
builder.Services.AddAutoMapper(typeof(MappingProfile));
var app = builder.Build();
app.Logger.LogInformation("App builded");
if (app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
//Test Automapper config.
TestAutoMapperConfig(app);
}
CreateDbIfNotExists(app);
// Configure the HTTP request pipeline.
app.MapGrpcService<BookMngService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client.");
app.Run();
}
private static void CreateDbIfNotExists(WebApplication app)
{
using (var scope = app.Services.CreateScope())
{
var sp = scope.ServiceProvider;
try
{
var dbContext = sp.GetRequiredService<BookingContext>();
//dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
BookingContextSeed.InitData(dbContext);
}
catch (Exception e)
{
app.Logger.LogError($"Error creating db - {e.Message}: {e.StackTrace} ");
}
}
}
private static void TestAutoMapperConfig(WebApplication app)
{
try
{
app.Logger.LogInformation("Validating Automapper mapping config...");
var mapper = app.Services.GetRequiredService<IMapper>();
mapper.ConfigurationProvider.AssertConfigurationIsValid();
app.Logger.LogInformation("Automapper mapping config is valid");
}
catch (Exception e)
{
app.Logger.LogError($"Error creating Mapping - {e.Message}: {e.StackTrace} ");
}
/*var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
mapperConfig.AssertConfigurationIsValid();*/
}
}
}
@@ -0,0 +1,25 @@
{
"profiles": {
"http": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5162"
},
"https": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7029;http://localhost:5162"
},
"Docker": {
"commandName": "Docker",
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true
}
}
}
+109
View File
@@ -0,0 +1,109 @@
syntax = "proto3";
option csharp_namespace = "DGM.HoC.BookMng.API.RPC";
package HoC;
service rpcBookMng {
rpc GetAllRoomTypes (EmptyReq) returns (GetAllRoomTypesResponse);
rpc GetRoomTypeById (RoomTypeId) returns (rpcRoomType);
rpc AddRoomType (rpcRoomType) returns (rpcRoomType);
rpc RemoveRoomType (RoomTypeId) returns (EmptyResponse);
rpc UpdateRoomType (rpcRoomType) returns (rpcRoomType);
rpc GetAllBookings (EmptyReq) returns (GetAllBookingsResponse);
rpc GetBookingById (BookingId) returns (rpcBooking);
rpc GetBookingDetailsById (BookingId) returns (rpcBookingDetails);
/*
Cuando creas un Booking no es sólo el booking,
también contiene una lista de RoomItems y el customerData.
Igual ocurre con el Remove, pues son dependientes.
*/
rpc AddBooking (rpcBookingDetails) returns (rpcBookingDetails);
rpc RemoveBooking (BookingId) returns (EmptyResponse);
rpc UpdateBooking (rpcBooking) returns (rpcBooking);
rpc GetRoomItemsByBooking (BookingId) returns (rpcRoomItemList);
rpc AddRoomItem (rpcRoomItem) returns (rpcRoomItem);
rpc RemoveRoomItem (RoomItemId) returns (EmptyResponse);
rpc UpdateRoomItem (rpcRoomItem) returns (rpcRoomItem);
// Los datos de clientes no se pueden crear ni eliminar, sólo actualizar.
// Dependen del booking.
rpc GetBookingCustomer (BookingId) returns (rpcBookingCustomer);
rpc UpdateBookingCustomer (rpcBookingCustomer) returns (rpcBookingCustomer);
}
message EmptyReq {}
message EmptyResponse {
string msg = 1;
}
message RoomTypeId {
int32 id = 1;
}
message RoomItemId {
int32 bookingId = 1;
int32 itemId = 2;
}
message BookingId {
int32 bookingId = 1;
}
message GetAllRoomTypesResponse {
repeated rpcRoomType roomTypeList = 1;
}
message GetAllBookingsResponse {
repeated rpcBooking bookingList = 1;
}
message rpcRoomType {
int32 id = 1;
string name = 2;
string description = 3;
int32 guestAmount = 4;
}
message rpcRoomItem {
int32 bookingId = 1;
int32 itemId = 2;
int32 guestAmount = 3;
string dateIn = 4;
string dateOut = 5;
float pricePerNight = 6;
string notes = 7;
int32 typeId = 8;
}
message rpcRoomItemList {
repeated rpcRoomItem roomItemList = 1;
}
message rpcBookingCustomer {
int32 bookingId = 1;
string name = 2;
string phone = 3;
string email = 4;
}
message rpcBooking {
int32 id = 1;
string description = 2;
string status = 3;
string customerId = 4;
}
message rpcBookingDetails {
rpcBooking data = 1;
rpcBookingCustomer bookingCustomer = 2;
repeated rpcRoomItem rooms = 3;
}
+21
View File
@@ -0,0 +1,21 @@
syntax = "proto3";
option csharp_namespace = "BookMng.API";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}
+279
View File
@@ -0,0 +1,279 @@
using AutoMapper;
using DGM.HoC.BookMng.API.Infrastructure;
using DGM.HoC.BookMng.API.Model;
using DGM.HoC.BookMng.API.RPC;
using Grpc.Core;
using Microsoft.AspNetCore.Cors.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.EntityFrameworkCore;
namespace DGM.HoC.BookMng.API.Services;
public class BookMngService : rpcBookMng.rpcBookMngBase
{
private readonly ILogger<BookMngService> _logger;
private readonly BookingContext _context;
private readonly IMapper _mapper;
public BookMngService(BookingContext context, ILogger<BookMngService> logger, IMapper mapper)
{
_context = context ?? throw new ArgumentNullException("context");
_logger = logger;
_mapper = mapper;
}
#region RoomType
public override async Task<GetAllRoomTypesResponse> GetAllRoomTypes(EmptyReq request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.GetAllRoomTypes");
var roomTypeList = await _context.RoomTypes.ToListAsync();
GetAllRoomTypesResponse response = new GetAllRoomTypesResponse();
foreach (var c in roomTypeList)
{
response.RoomTypeList.Add(_mapper.Map<RoomType, rpcRoomType>(c));
}
return response;
}
public override async Task<rpcRoomType> GetRoomTypeById(RoomTypeId request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.GetRoomTypeById");
var roomType = await _context.RoomTypes.FindAsync(request.Id);
if (roomType == null)
{
_logger.LogWarning("BookMngService.GetRoomTypeById - RoomType not found");
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found RoomType with ID {request.Id}"));
}
return _mapper.Map<RoomType, rpcRoomType>(roomType);
}
public override async Task<rpcRoomType> AddRoomType(rpcRoomType request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.AddRoomType");
try
{
var roomTypeToAdd = _mapper.Map<rpcRoomType, RoomType>(request);
//Check if ID already exists. Find podría ser Async...
if (_context.RoomTypes.Find(roomTypeToAdd.Id) != null)
{
throw new RpcException(new Status(StatusCode.AlreadyExists, $"Already exists RoomType with ID {roomTypeToAdd.Id}"));
}
//No hacer async para el Add.
var item = _context.RoomTypes.Add(roomTypeToAdd);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"BookMngService.AddRoomType: Added {rows} RoomTypes");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.DataLoss, "RoomType can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"RoomType with ID: {roomTypeToAdd.Id} added");
return _mapper.Map<RoomType, rpcRoomType>(item.Entity);
}
catch (Exception e)
{
_logger.LogError($"BookMngService.GetBookingById - Error adding RoomType: {e.Message} - {e.StackTrace}");
throw new RpcException(new Status(StatusCode.Aborted, "Error adding RoomType: " + e.Message));
}
}
#endregion
#region Bookings
public override async Task<GetAllBookingsResponse> GetAllBookings(EmptyReq request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.GetAllBookings");
var bookingList = await _context.Bookings.ToListAsync();
GetAllBookingsResponse response = new GetAllBookingsResponse();
foreach (var b in bookingList)
{
response.BookingList.Add(_mapper.Map<Booking, rpcBooking>(b));
}
return response;
}
public override async Task<rpcBooking> GetBookingById(BookingId request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.GetBookingById");
var booking = await _context.Bookings.FindAsync(request.BookingId_);
if (booking == null)
{
_logger.LogWarning("BookMngService.GetBookingById - Booking not found");
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found Booking with ID {request.BookingId_}"));
}
return _mapper.Map<Booking, rpcBooking>(booking);
}
public override async Task<rpcBookingDetails> AddBooking(rpcBookingDetails request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.AddBooking");
try
{
var bookingToAdd = _mapper.Map<rpcBookingDetails, Booking>(request);
//Check if ID already exists. Find podría ser Async...
if (_context.Bookings.Find(bookingToAdd.Id) != null)
{
throw new RpcException(new Status(StatusCode.AlreadyExists, $"Already exists Booking with ID {bookingToAdd.Id}"));
}
//No hacer async para el Add.
var item = _context.Bookings.Add(bookingToAdd);
//Añadir CustomerData en caso de tenerlo.
if(bookingToAdd.BookingCustomer != null)
{
// NOTA: Comprobar si funciona esto, o hay que conseguir el ID de otra forma. En cualquier caso, no se debería de poder recibir un ID desde gRPC para la creación
//Actualizamos el ID.
bookingToAdd.BookingCustomer.BookingId = item.Entity.Id;
_context.BookingCustomerData.Add(bookingToAdd.BookingCustomer);
}
foreach(var room in bookingToAdd.Rooms)
{
room.BookingId = item.Entity.Id;
_context.RoomItems.Add(room);
}
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"BookMngService.AddBooking: Added {rows} Booking");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.DataLoss, "Booking can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Booking with ID: {bookingToAdd.Id} added");
return _mapper.Map<Booking, rpcBookingDetails>(item.Entity);
}
catch (Exception e)
{
_logger.LogError($"BookMngService.GetBookingById - Error adding Booking: {e.Message} - {e.StackTrace}");
throw new RpcException(new Status(StatusCode.Aborted, "Error adding Booking: " + e.Message));
}
}
#endregion
#region BookingItem
public override async Task<rpcRoomItemList> GetRoomItemsByBooking(BookingId request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.GetRoomItemsByBooking");
var roomList = await _context.RoomItems.Where(ri => ri.BookingId == request.BookingId_).ToListAsync();
rpcRoomItemList response = new rpcRoomItemList();
foreach (var ri in roomList)
{
response.RoomItemList.Add(_mapper.Map<RoomItem, rpcRoomItem>(ri));
}
return response;
}
public override async Task<rpcRoomItem> AddRoomItem(rpcRoomItem request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.AddRoomItem");
try
{
var roomItemToAdd = _mapper.Map<rpcRoomItem, RoomItem>(request);
//Check if ID already exists. Find podría ser Async...
if (_context.RoomItems.Find(roomItemToAdd.Id, roomItemToAdd.BookingId) != null)
{
throw new RpcException(new Status(StatusCode.AlreadyExists, $"Already exists RoomItem with ID: {roomItemToAdd.BookingId} - {roomItemToAdd.Id}"));
}
//Get Last ID for given booking.
var lastItem = await _context.RoomItems.Where(ri => ri.BookingId == request.BookingId).OrderBy(ri => ri.Id).LastOrDefaultAsync();
roomItemToAdd.Id = lastItem is null ? 1 : lastItem.Id;
//No hacer async para el Add.
var item = _context.RoomItems.Add(roomItemToAdd);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"BookMngService.AddRoomItem: Added {rows} RoomItem");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.DataLoss, "RoomItem can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"RoomItem with ID: {roomItemToAdd.BookingId} - {roomItemToAdd.Id} added");
return _mapper.Map<RoomItem, rpcRoomItem>(item.Entity);
}
catch (Exception e)
{
throw new RpcException(new Status(StatusCode.Aborted, "Error adding RoomItem: " + e.Message));
}
}
public override async Task<EmptyResponse> RemoveRoomItem(RoomItemId request, ServerCallContext context)
{
_logger.LogInformation("Begin grpc call BookMngService.RemoveRoomItem");
try
{
RoomItem? roomItem = await _context.RoomItems.FindAsync(request.BookingId, request.ItemId);
if (roomItem == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"RoomItem ID: {request.BookingId} - {request.ItemId} not fount"));
}
_context.RoomItems.Remove(roomItem);
int rows = await _context.SaveChangesAsync();
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.DataLoss, "RoomItem can't be deleted"));
}
context.Status = new Status(StatusCode.OK, $"RoomItem with ID: {request.BookingId} - {request.ItemId} deleted");
return new EmptyResponse() { Msg = $"RoomItem Deleted - ID {request.BookingId} - {request.ItemId}" };
}
catch (Exception e)
{
throw new RpcException(new Status(StatusCode.Aborted, "Error removing RoomItem: " + e.Message));
}
}
#endregion
#region BookingCustomerData
public override Task<rpcBookingCustomer> GetBookingCustomer(BookingId request, ServerCallContext context)
{
return base.GetBookingCustomer(request, context);
}
#endregion
}
+22
View File
@@ -0,0 +1,22 @@
using BookMng.API;
using Grpc.Core;
namespace BookMng.API.Services
{
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}
+34
View File
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UserSecretsId>9c59ecba-0968-470b-8791-9d83a1b8c2f1</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<RootNamespace>DGM.HoC.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="Protos\CRM.proto" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\CRM.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Grpc.AspNetCore" Version="2.56.0" />
<PackageReference Include="Grpc.AspNetCore.Server.Reflection" Version="2.56.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.10" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.10">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.19.5" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
</ItemGroup>
</Project>
+22
View File
@@ -0,0 +1,22 @@
#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:7.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src
COPY ["CRM.API/CRM.API.csproj", "CRM.API/"]
RUN dotnet restore "CRM.API/CRM.API.csproj"
COPY . .
WORKDIR "/src/CRM.API"
RUN dotnet build "CRM.API.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "CRM.API.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "CRM.API.dll"]
+23
View File
@@ -0,0 +1,23 @@
using DGM.HoC.CRM.API.Infrastructure.EntityConfigs;
using DGM.HoC.CRM.API.Model;
using Microsoft.EntityFrameworkCore;
namespace DGM.HoC.CRM.API.Infrastructure;
public class CRMContext : DbContext
{
public CRMContext(DbContextOptions<CRMContext> options)
: base(options)
{ }
public DbSet<Customer> Customers { get; set; }
public DbSet<ContactDetails> ContactDetails { get; set; }
public DbSet<Note> Notes { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new CustomerTypeEntityConfig());
base.OnModelCreating(modelBuilder);
}
}
+103
View File
@@ -0,0 +1,103 @@
using DGM.HoC.CRM.API.Model;
namespace DGM.HoC.CRM.API.Infrastructure
{
public class CRMContextSeed
{
public static void InitData(CRMContext dbContext)
{
if(dbContext.Customers.Count() == 0)
{
InitCustomerData(dbContext);
dbContext.SaveChanges();
}
if (dbContext.Notes.Count() == 0)
{
InitCustomerNotes(dbContext);
dbContext.SaveChanges();
}
}
public static void InitCustomerData(CRMContext dbContext)
{
dbContext.Customers.Add(new Model.Customer
{
NIF = "N12345",
Name = "Bilbo Bolsón",
CustomerAddress = new Address
{
TipoVia = "Ciudad",
Poblacion = "Rivendel"
},
CreateTime = DateTime.Now,
LastUsedTime = DateTime.Now
});
dbContext.Customers.Add(new Model.Customer
{
NIF = "N98765",
Name = "Frodo Bolsón",
CustomerAddress = new Address
{
TipoVia = "Ciudad",
Poblacion = "Bolson Cerrado"
},
CreateTime = DateTime.Now,
LastUsedTime = DateTime.Now
});
dbContext.Customers.Add(new Model.Customer
{
NIF = "N567432",
Name = "Gandalf el Gris",
CustomerAddress = new Address(),
CreateTime = DateTime.Now,
LastUsedTime = DateTime.Now
});
}
public static void InitCustomerNotes(CRMContext dbContext)
{
dbContext.Notes.Add(new Model.Note
{
Id = 1,
CustomerNIF = "N12345",
Status = "Abierto",
Data = "Solicitud de pergamino y material de escritura",
CreateTime = DateTime.Now
});
dbContext.Notes.Add(new Model.Note
{
Id = 2,
CustomerNIF = "N98765",
Status = "Cerrado",
Data = "Incluir establo para caballos",
CreateTime = DateTime.Now
});
dbContext.Notes.Add(new Model.Note
{
Id = 3,
CustomerNIF = "N98765",
Status = "Abierto",
Data = "Vendrá Sam a pagar la semana que viene",
CreateTime = DateTime.Now
});
dbContext.Notes.Add(new Model.Note
{
Id = 4,
CustomerNIF = "N567432",
Status = "Cerrado",
Data = "Nos ha dejado un recado para un hobbit",
CreateTime = DateTime.Now
});
}
}
}
@@ -0,0 +1,32 @@
using DGM.HoC.CRM.API.Model;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace DGM.HoC.CRM.API.Infrastructure.EntityConfigs;
public class CustomerTypeEntityConfig : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
//PK
builder.HasKey(e => e.NIF);
//Address Data Class.
builder.OwnsOne(e => e.CustomerAddress);
//FK - ContactData
builder.HasMany(e => e.ContactDetails)
.WithOne(cd => cd.Customer)
.HasForeignKey(cd => cd.CustomerNIF)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
//FK - Notes
builder.HasMany(e => e.Notes)
.WithOne(note => note.Customer)
.HasForeignKey(note => note.CustomerNIF)
.IsRequired()
.OnDelete(DeleteBehavior.Cascade);
}
}
@@ -0,0 +1,56 @@
using AutoMapper;
using Google.Protobuf.WellKnownTypes;
using System.Globalization;
using System.Reflection.Metadata.Ecma335;
namespace DGM.HoC.CRM.API.Infrastructure.Mappers
{
public class StringToDateTimeConverter : ITypeConverter<string, DateTime>
{
public DateTime Convert(string source, DateTime destination, ResolutionContext context)
{
var esES = new CultureInfo("es-ES");
DateTime dateTime;
if (DateTime.TryParseExact(source, "dd/MM/yyyy", esES, DateTimeStyles.AllowLeadingWhite, out dateTime))
{
return dateTime;
}
return DateTime.UnixEpoch;
}
}
public class DateTimeToStringConverter : ITypeConverter<DateTime, string>
{
public string Convert(DateTime source, string destination, ResolutionContext context)
{
return source.ToString();
}
}
public class DateTimeToTimeStamp : ITypeConverter<DateTime, Google.Protobuf.WellKnownTypes.Timestamp>
{
public Timestamp Convert(DateTime source, Timestamp destination, ResolutionContext context)
{
//Timestamp.FromDateTime() requiere el time en UTC.
DateTime utcTime = source.Kind == DateTimeKind.Utc ? source : source.ToUniversalTime();
return Timestamp.FromDateTime(utcTime);
}
}
public class TimeStampToDateTime : ITypeConverter<Google.Protobuf.WellKnownTypes.Timestamp, DateTime>
{
public DateTime Convert(Timestamp source, DateTime destination, ResolutionContext context)
{
if(source is null)
{
return DateTime.UnixEpoch;
}
return source.ToDateTime();
}
}
}
@@ -0,0 +1,49 @@
using AutoMapper;
using DGM.HoC.CRM.API.Model;
using DGM.HoC.CRM.API.RPC;
namespace DGM.HoC.CRM.API.Infrastructure.Mappers
{
public class MappingProfile : Profile
{
public MappingProfile()
{
//DateTime <--> String Mapping.
CreateMap<string, DateTime>().ConvertUsing<StringToDateTimeConverter>();
CreateMap<DateTime, string>().ConvertUsing<DateTimeToStringConverter>();
CreateMap<Google.Protobuf.WellKnownTypes.Timestamp, DateTime>().ConvertUsing<TimeStampToDateTime>();
CreateMap<DateTime, Google.Protobuf.WellKnownTypes.Timestamp>().ConvertUsing<DateTimeToTimeStamp>();
CreateMap<rpcAddress, Address>();
CreateMap<Address, rpcAddress>().ForAllMembers(map => map.NullSubstitute(""));
CreateMap<ContactDetails, rpcContact>(MemberList.Destination);
CreateMap<rpcContact, ContactDetails>(MemberList.Source);
CreateMap<Customer, rpcCustomer>(MemberList.Destination);
CreateMap<rpcCustomer, Customer>(MemberList.Source);
CreateMap<Note, rpcNote>(MemberList.Destination);
CreateMap<rpcNote, Note>(MemberList.Source);
CreateMap<Customer, rpcAddCustomerReq>(MemberList.Destination);
CreateMap<Customer, rpcUpdateCustomerReq>(MemberList.Destination);
CreateMap<rpcAddCustomerReq, Customer>(MemberList.Source);
//CreateMap<rpcUpdateCustomerReq, Customer>(MemberList.None);
CreateMap<Note, rpcAddNoteReq>(MemberList.Destination);
CreateMap<Note, rpcUpdateNoteReq>(MemberList.Destination);
CreateMap<rpcAddNoteReq, Note>(MemberList.Source);
//CreateMap<rpcUpdateNoteReq, Note>(MemberList.None);
CreateMap<ContactDetails, rpcAddContactDetailsReq>(MemberList.Destination);
CreateMap<ContactDetails, rpcUpdateContactDetailsReq>(MemberList.Destination);
CreateMap<rpcAddContactDetailsReq, ContactDetails>(MemberList.Source);
//CreateMap<rpcUpdateContactDetailsReq, ContactDetails>(MemberList.None);
}
}
}
@@ -0,0 +1,146 @@
// <auto-generated />
using System;
using DGM.HoC.CRM.API.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DGM.HoC.CRM.API.Migrations
{
[DbContext(typeof(CRMContext))]
[Migration("20230901133350_addressData-Tests")]
partial class addressDataTests
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Customer", b =>
{
b.Property<string>("NIF")
.HasColumnType("varchar(255)");
b.Property<DateTime>("CreateDate")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("LastUsedDate")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Phone")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("NIF");
b.ToTable("Customers");
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Note", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<string>("CustomerNIF")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("CustomerNIF");
b.ToTable("Notes");
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Customer", b =>
{
b.OwnsOne("DGM.HoC.CRM.API.Model.Address", "AddressData", b1 =>
{
b1.Property<string>("CustomerNIF")
.HasColumnType("varchar(255)");
b1.Property<string>("CodigoPostal")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("NombreVia")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Numero")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Pais")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Poblacion")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Provincia")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("TipoVia")
.IsRequired()
.HasColumnType("longtext");
b1.HasKey("CustomerNIF");
b1.ToTable("Customers");
b1.WithOwner()
.HasForeignKey("CustomerNIF");
});
b.Navigation("AddressData")
.IsRequired();
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Note", b =>
{
b.HasOne("DGM.HoC.CRM.API.Model.Customer", "Customer")
.WithMany("Notes")
.HasForeignKey("CustomerNIF")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Customer", b =>
{
b.Navigation("Notes");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,95 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace DGM.HoC.CRM.API.Migrations
{
/// <inheritdoc />
public partial class addressDataTests : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Customers",
columns: table => new
{
NIF = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Name = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_TipoVia = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_NombreVia = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_Numero = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_Provincia = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_Poblacion = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_CodigoPostal = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
AddressData_Pais = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Phone = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Email = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreateDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
LastUsedDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Customers", x => x.NIF);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "Notes",
columns: table => new
{
Id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
CustomerNIF = table.Column<string>(type: "varchar(255)", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
Data = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4"),
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
Status = table.Column<string>(type: "longtext", nullable: false)
.Annotation("MySql:CharSet", "utf8mb4")
},
constraints: table =>
{
table.PrimaryKey("PK_Notes", x => x.Id);
table.ForeignKey(
name: "FK_Notes_Customers_CustomerNIF",
column: x => x.CustomerNIF,
principalTable: "Customers",
principalColumn: "NIF",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_Notes_CustomerNIF",
table: "Notes",
column: "CustomerNIF");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Notes");
migrationBuilder.DropTable(
name: "Customers");
}
}
}
@@ -0,0 +1,143 @@
// <auto-generated />
using System;
using DGM.HoC.CRM.API.Infrastructure;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace DGM.HoC.CRM.API.Migrations
{
[DbContext(typeof(CRMContext))]
partial class CRMContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.10")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Customer", b =>
{
b.Property<string>("NIF")
.HasColumnType("varchar(255)");
b.Property<DateTime>("CreateDate")
.HasColumnType("datetime(6)");
b.Property<string>("Email")
.IsRequired()
.HasColumnType("longtext");
b.Property<DateTime>("LastUsedDate")
.HasColumnType("datetime(6)");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Phone")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("NIF");
b.ToTable("Customers");
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Note", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<string>("CustomerNIF")
.IsRequired()
.HasColumnType("varchar(255)");
b.Property<string>("Data")
.IsRequired()
.HasColumnType("longtext");
b.Property<string>("Status")
.IsRequired()
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("CustomerNIF");
b.ToTable("Notes");
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Customer", b =>
{
b.OwnsOne("DGM.HoC.CRM.API.Model.Address", "AddressData", b1 =>
{
b1.Property<string>("CustomerNIF")
.HasColumnType("varchar(255)");
b1.Property<string>("CodigoPostal")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("NombreVia")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Numero")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Pais")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Poblacion")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("Provincia")
.IsRequired()
.HasColumnType("longtext");
b1.Property<string>("TipoVia")
.IsRequired()
.HasColumnType("longtext");
b1.HasKey("CustomerNIF");
b1.ToTable("Customers");
b1.WithOwner()
.HasForeignKey("CustomerNIF");
});
b.Navigation("AddressData")
.IsRequired();
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Note", b =>
{
b.HasOne("DGM.HoC.CRM.API.Model.Customer", "Customer")
.WithMany("Notes")
.HasForeignKey("CustomerNIF")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Customer");
});
modelBuilder.Entity("DGM.HoC.CRM.API.Model.Customer", b =>
{
b.Navigation("Notes");
});
#pragma warning restore 612, 618
}
}
}
+44
View File
@@ -0,0 +1,44 @@
using Microsoft.EntityFrameworkCore;
using System.Reflection;
namespace DGM.HoC.CRM.API.Model
{
[Owned]
public class Address
{
public string? TipoVia { get; set; }
public string? NombreVia { get; set; }
public string? Numero { get; set; }
public string? Provincia { get; set; }
public string? Poblacion { get; set; }
public string? CodigoPostal { get; set; }
public string? Pais { get; set; }
public override bool Equals(object? obj)
{
Type AddressType = GetType();
if(obj is null || obj.GetType() != AddressType)
{
return false;
}
var props = AddressType.GetProperties();
foreach(var p in props)
{
if(p.GetValue(obj) != p.GetValue(this))
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
}
}
+21
View File
@@ -0,0 +1,21 @@
namespace DGM.HoC.CRM.API.Model;
public class ContactDetails
{
public int Id { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public string? Comment { get; set; }
public ContactDetails(string type, string value, string? comment)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
Value = value ?? throw new ArgumentNullException(nameof(value));
Comment = comment;
}
public string CustomerNIF { get; set; }
public Customer Customer { get; }
}
+20
View File
@@ -0,0 +1,20 @@
using System.ComponentModel.DataAnnotations;
namespace DGM.HoC.CRM.API.Model;
public class Customer
{
public string NIF { get; set; }
public string Name { get; set; }
public Address CustomerAddress { get; set; }
public ICollection<ContactDetails> ContactDetails { get; } = new List<ContactDetails>();
public DateTime CreateTime { get; set; }
public DateTime LastUsedTime { get; set; }
public ICollection<Note> Notes { get; } = new List<Note>();
}
+15
View File
@@ -0,0 +1,15 @@
namespace DGM.HoC.CRM.API.Model;
public class Note
{
public int Id { get; set; }
public string CustomerNIF { get; set; }
public string Data { get; set; }
public string Status { get; set; }
public DateTime CreateTime { get; set; }
public Customer Customer { get; }
}
+91
View File
@@ -0,0 +1,91 @@
using AutoMapper;
using DGM.HoC.CRM.API.Infrastructure;
using DGM.HoC.CRM.API.Infrastructure.Mappers;
using DGM.HoC.CRM.API.Services;
using Microsoft.AspNetCore.Hosting.Server;
using Microsoft.EntityFrameworkCore;
namespace DGM.HoC.CRM.API;
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();
builder.Services.AddDbContext<CRMContext>(options =>
{
string? conn = builder.Environment.IsDevelopment() ? builder.Configuration["HOC:CRM:DB"] : builder.Configuration["HOC_DBString"];
options.UseMySql(conn, new MariaDbServerVersion(new Version(10, 11, 2)), o => o.EnableRetryOnFailure());
});
builder.Services.AddAutoMapper(typeof(MappingProfile));
var app = builder.Build();
app.Logger.LogInformation("App builded");
if(app.Environment.IsDevelopment())
{
app.MapGrpcReflectionService();
//Test Automapper config.
TestAutoMapperConfig(app);
}
CreateDbIfNotExists(app);
// Configure the HTTP request pipeline.
app.MapGrpcService<CRMService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client.");
app.Run();
}
private static void CreateDbIfNotExists(WebApplication app)
{
using (var scope = app.Services.CreateScope())
{
var sp = scope.ServiceProvider;
try
{
var dbContext = sp.GetRequiredService<CRMContext>();
//dbContext.Database.EnsureDeleted();
dbContext.Database.EnsureCreated();
//dbContext.Database.Migrate();
if(app.Environment.IsDevelopment())
{
CRMContextSeed.InitData(dbContext);
}
}
catch (Exception e)
{
app.Logger.LogError($"Error creating db - {e.Message}: {e.StackTrace} ");
}
}
}
private static void TestAutoMapperConfig(WebApplication app)
{
try
{
app.Logger.LogInformation("Validating Automapper mapping config...");
var mapper = app.Services.GetRequiredService<IMapper>();
mapper.ConfigurationProvider.AssertConfigurationIsValid();
app.Logger.LogInformation("Automapper mapping config is valid");
}
catch (Exception e)
{
app.Logger.LogError($"Error creating Mapping - {e.Message}: {e.StackTrace} ");
}
/*var mapperConfig = new MapperConfiguration(cfg =>
{
cfg.AddProfile<MappingProfile>();
});
mapperConfig.AssertConfigurationIsValid();*/
}
}
+25
View File
@@ -0,0 +1,25 @@
{
"profiles": {
"http": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "http://localhost:5201"
},
"https": {
"commandName": "Project",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dotnetRunMessages": true,
"applicationUrl": "https://localhost:7061;http://localhost:5201"
},
"Docker": {
"commandName": "Docker",
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true
}
}
}
+164
View File
@@ -0,0 +1,164 @@
syntax = "proto3";
option csharp_namespace = "DGM.HoC.CRM.API.RPC";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
package HoC;
service rpcCRM {
rpc GetCustomerById (CustomerIdReq) returns (rpcCustomer);
rpc GetAllCustomers (google.protobuf.Empty) returns (CustomerListResponse);
rpc GetCustomersByFilter (rpcCustomerFilter) returns (CustomerListResponse);
rpc AddCustomer (rpcAddCustomerReq) returns (rpcCustomer);
rpc RemoveCustomer (CustomerIdReq) returns (google.protobuf.Empty);
rpc UpdateCustomer (rpcUpdateCustomerReq) returns (rpcCustomer);
rpc GetAllCustomerContactDetails (google.protobuf.Empty) returns (ContactDetailsListResponse);
rpc GetContactDetailsByCustomer (CustomerIdReq) returns (ContactDetailsListResponse);
rpc GetContactDetailsById (ContactIdReq) returns (rpcContact);
rpc AddCustomerContact (rpcAddContactDetailsReq) returns (rpcContact);
rpc RemoveCustomerContact (ContactIdReq) returns (EmptyResponse);
rpc UpdateCustomerContact (rpcUpdateContactDetailsReq) returns (rpcContact);
rpc GetNoteById (NoteIdReq) returns (rpcNote);
rpc GetNoteByCustomer (CustomerIdReq) returns (NoteListResponse);
rpc GetAllNotes (EmptyReq) returns (NoteListResponse);
rpc AddNote (rpcAddNoteReq) returns (rpcNote);
rpc RemoveNote (NoteIdReq) returns (EmptyResponse);
rpc UpdateNote (rpcUpdateNoteReq) returns (rpcNote);
}
message EmptyReq {}
message EmptyResponse {
string msg = 1;
}
// Requests messages
message CustomerIdReq {
string customerId = 1;
}
message ContactIdReq {
int32 contactId = 1;
}
message NoteIdReq {
int32 noteId = 1;
}
message rpcAddCustomerReq {
string nif = 1;
string name = 2;
rpcAddress customerAddress = 3;
}
message rpcUpdateCustomerReq {
string nif = 1;
optional string name = 2;
optional rpcAddress customerAddress = 3;
}
message rpcAddContactDetailsReq {
string customerNIF = 2;
string type = 5;
string value = 6;
string comment = 7;
}
message rpcUpdateContactDetailsReq {
int32 id = 1;
optional string customerNIF = 2;
optional string type = 5;
optional string value = 6;
optional string comment = 7;
}
message rpcAddNoteReq {
string customerNIF = 2;
string data = 5;
string status = 6;
}
message rpcUpdateNoteReq {
int32 id = 1;
optional string customerNIF = 2;
optional string data = 5;
optional string status = 6;
}
// Response messages
message CustomerListResponse {
repeated rpcCustomer customerList = 1;
}
message ContactDetailsListResponse {
repeated rpcContact contactList = 1;
}
message NoteListResponse {
repeated rpcNote noteList = 1;
}
// Entity messages
message rpcCustomer {
string nif = 1;
string name = 2;
rpcAddress customerAddress = 3;
optional google.protobuf.Timestamp createTime = 6;
optional google.protobuf.Timestamp lastUsedTime = 7;
}
message rpcCustomerFilter {
string nif = 1;
string name = 2;
string contactData = 3;
bool strict = 10;
optional int32 pageIndex = 11;
optional int32 pageSize = 12;
}
message rpcAddress {
optional string tipoVia = 1;
optional string nombreVia = 2;
optional string numero = 3;
optional string provincia = 4;
optional string poblacion = 5;
optional string codigoPostal = 6;
optional string pais = 7;
}
message rpcContact {
string customerNIF = 1;
int32 id = 2;
string type = 3;
string value = 4;
string comment = 5;
}
message rpcNote {
string customerNIF = 1;
int32 id = 2;
string data = 3;
string status = 4;
optional google.protobuf.Timestamp createTime = 5;
}
+560
View File
@@ -0,0 +1,560 @@
namespace DGM.HoC.CRM.API.Services;
using AutoMapper;
using DGM.HoC.CRM.API.Infrastructure;
using DGM.HoC.CRM.API.Infrastructure.Mappers;
using DGM.HoC.CRM.API.Model;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
using RPC;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
public class CRMService : rpcCRM.rpcCRMBase
{
private readonly CRMContext _context;
private readonly ILogger<CRMService> _logger;
private readonly IMapper _mapper;
public CRMService(CRMContext context, ILogger<CRMService> logger, IMapper mapper)
{
_context = context ?? throw new ArgumentNullException(nameof(context));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mapper = mapper ?? throw new ArgumentNullException(nameof(mapper));
}
#region Customers
#region Customers Queries
public override async Task<rpcCustomer> GetCustomerById(CustomerIdReq request, ServerCallContext context)
{
LogCall();
var customer = await _context.Customers.FindAsync(request.CustomerId);
if (customer == null)
{
_logger.LogWarning("CRMService.GetCustomerById - Customer not found");
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found Customer with ID {request.CustomerId}"));
}
return _mapper.Map<Customer, rpcCustomer>(customer);
}
public override async Task<CustomerListResponse> GetAllCustomers(Empty request, ServerCallContext context)
{
LogCall();
var customerList = await _context.Customers.ToListAsync();
CustomerListResponse response = new CustomerListResponse();
foreach (var c in customerList)
{
response.CustomerList.Add(_mapper.Map<Customer, rpcCustomer>(c));
}
return response;
}
public override async Task<CustomerListResponse> GetCustomersByFilter(rpcCustomerFilter request, ServerCallContext context)
{
LogCall();
var query = _context.Customers.Where(
c => c.NIF.Contains(request.Nif)
&& c.Name.Contains(request.Name));
if (!string.IsNullOrWhiteSpace(request.ContactData))
{
query = query.Where(c => c.ContactDetails.Any(ct => ct.Value.Contains(request.ContactData)));
}
if (request.HasPageSize && request.HasPageIndex)
{
query = query.Skip(request.PageSize * request.PageIndex).Take(request.PageSize);
}
var list = await query.ToListAsync();
var rs = new CustomerListResponse();
rs.CustomerList.AddRange(list.Select(c => _mapper.Map<rpcCustomer>(c)));
return rs;
}
#endregion
public override async Task<rpcCustomer> AddCustomer(rpcAddCustomerReq request, ServerCallContext context)
{
LogCall();
try
{
var customerToAdd = _mapper.Map<rpcAddCustomerReq, Customer>(request);
if(String.IsNullOrWhiteSpace(customerToAdd.NIF))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, $"Customer ID cant be empty"));
}
//Check if ID already exists. Find podría ser Async...
if (_context.Customers.Find(customerToAdd.NIF) != null)
{
throw new RpcException(new Status(StatusCode.AlreadyExists, $"Already exists Customer with ID {customerToAdd.NIF}"));
}
DateTime now = DateTime.UtcNow;
customerToAdd.CreateTime = now;
customerToAdd.LastUsedTime = now;
//No hacer async para el Add.
var item = _context.Customers.Add(customerToAdd);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.AddCustomer: Added {rows} customers");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Customer can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Customer with ID: {customerToAdd.NIF} added");
return _mapper.Map<Customer, rpcCustomer>(item.Entity);
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error adding Customer: " + e.Message));
}
}
public override async Task<Empty> RemoveCustomer(CustomerIdReq request, ServerCallContext context)
{
LogCall(); try
{
Customer? c = await _context.Customers.FindAsync(request.CustomerId);
if(c == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Customer ID: {request.CustomerId} not fount"));
}
_context.Customers.Remove(c);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.RemoveCustomer: Removed {rows} customers");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Customer can't be deleted"));
}
context.Status = new Status(StatusCode.OK, $"Customer with ID: {request.CustomerId} deleted");
return new Empty();
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error removing Customer: " + e.Message));
}
}
public override async Task<rpcCustomer> UpdateCustomer(rpcUpdateCustomerReq request, ServerCallContext context)
{
LogCall();
try
{
//var updatedCustomer = _mapper.Map<rpcUpdateCustomerReq, Customer>(request);
//Check if ID exists. Find podría ser Async...
var customer = _context.Customers.Find(request.Nif);
if (customer is null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Customer with ID {request.Nif} Not Found"));
}
//Update Entity.
bool updated = false;
if (request.HasName && request.Name != customer.Name) { customer.Name = request.Name; updated = true; }
if (request.CustomerAddress is not null)
{
var newAddress = _mapper.Map<rpcAddress, Address>(request.CustomerAddress);
if(customer.CustomerAddress != newAddress)
{
customer.CustomerAddress = newAddress;
updated = true;
}
}
if(updated)
{
customer.LastUsedTime = DateTime.UtcNow;
}
//var item = _context.Customers.Update(updatedCustomer);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.UpdateCustomerData: Updated {rows} customers");
if (updated && rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Customer can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Customer with ID: {request.Nif} updated");
return _mapper.Map<Customer, rpcCustomer>(customer);
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error updating Customer: " + e.Message));
}
}
#endregion
#region Customer Contact Details
#region Customers Contact Details Queries
public override async Task<ContactDetailsListResponse> GetAllCustomerContactDetails(Empty request, ServerCallContext context)
{
LogCall();
var contactList = await _context.ContactDetails.ToListAsync();
ContactDetailsListResponse response = new ContactDetailsListResponse();
foreach (var n in contactList)
{
response.ContactList.Add(_mapper.Map<ContactDetails, rpcContact>(n));
}
return response;
}
public override async Task<ContactDetailsListResponse> GetContactDetailsByCustomer(CustomerIdReq request, ServerCallContext context)
{
LogCall();
var contactList = _context.ContactDetails.Where(e => e.CustomerNIF == request.CustomerId);
if (contactList == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found Contacts for customer with ID {request.CustomerId}"));
}
ContactDetailsListResponse response = new ContactDetailsListResponse();
foreach (var n in contactList)
{
response.ContactList.Add(_mapper.Map<ContactDetails, rpcContact>(n));
}
return response;
}
public override async Task<rpcContact> GetContactDetailsById(ContactIdReq request, ServerCallContext context)
{
LogCall();
ContactDetails? note = await _context.ContactDetails.FindAsync(request.ContactId);
if (note == null)
{
_logger.LogWarning("CRMService.GetContactDetailsById - Contact not found");
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found Contact with ID {request.ContactId}"));
}
return _mapper.Map<ContactDetails, rpcContact>(note);
}
#endregion
public override async Task<rpcContact> AddCustomerContact(rpcAddContactDetailsReq request, ServerCallContext context)
{
LogCall();
try
{
var ContactToAdd = _mapper.Map<rpcAddContactDetailsReq, ContactDetails>(request);
if (String.IsNullOrWhiteSpace(ContactToAdd.CustomerNIF))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, $"Invalid customer ID"));
}
//No hacer async para el Add.
var item = _context.ContactDetails.Add(ContactToAdd);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.AddCustomerContact: Added {rows} contacts");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Note can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Contact with ID: {ContactToAdd.Id} added");
return _mapper.Map<ContactDetails, rpcContact>(item.Entity);
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error adding Contact: " + e.Message));
}
}
public override async Task<EmptyResponse> RemoveCustomerContact(ContactIdReq request, ServerCallContext context)
{
LogCall(); try
{
ContactDetails? n = await _context.ContactDetails.FindAsync(request.ContactId);
if (n == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Contact with ID: {request.ContactId} not fount"));
}
_context.ContactDetails.Remove(n);
int rows = await _context.SaveChangesAsync();
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Contact can't be deleted"));
}
context.Status = new Status(StatusCode.OK, $"Contact with ID: {request.ContactId} deleted");
return new EmptyResponse() { Msg = $"Contact Deleted - ID {request.ContactId}" };
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error removing Contact: " + e.Message));
}
}
public override async Task<rpcContact> UpdateCustomerContact(rpcUpdateContactDetailsReq request, ServerCallContext context)
{
LogCall();
try
{
//var updatedContact = _mapper.Map<rpcUpdateContactDetailsReq, ContactDetails>(request);
//Check if ID already exists. Find podría ser Async...
var contact = await _context.ContactDetails.FindAsync(request.Id);
if (contact is null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Contact with ID {request.Id} Not Found"));
}
//Update Entity.
bool updated = false;
if (request.HasCustomerNIF && request.CustomerNIF != contact.CustomerNIF) { contact.CustomerNIF = request.CustomerNIF; updated = true; }
if (request.HasType && request.Type != contact.Type) { contact.Type = request.Type; updated = true; }
if (request.HasValue && request.Value != contact.Value) { contact.Value = request.Value; updated = true; }
if (request.HasComment && request.Comment != contact.Comment) { contact.Comment = request.Comment; updated = true; }
//var item = _context.Customers.Update(updatedCustomer);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.UpdateCustomerContact: Updated {rows} Contacts");
if (updated && rows < 1)
{
throw new RpcException(new Status(StatusCode.DataLoss, "Contact can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Contact with ID: {request.Id} updated");
return _mapper.Map<ContactDetails, rpcContact>(contact);
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Aborted, "Error updating Contact: " + e.Message));
}
}
#endregion
#region Customer Notes
#region Customer Notes Queries
public override async Task<rpcNote> GetNoteById(NoteIdReq request, ServerCallContext context)
{
LogCall();
Note? note = await _context.Notes.FindAsync(request.NoteId);
if (note == null)
{
_logger.LogWarning("CRMService.GetNoteById - Note not found");
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found Note with ID {request.NoteId}"));
}
return _mapper.Map<Note, rpcNote>(note);
}
public override Task<NoteListResponse> GetNoteByCustomer(CustomerIdReq request, ServerCallContext context)
{
LogCall();
var noteList = _context.Notes.Where(e => e.CustomerNIF == request.CustomerId);
if (noteList == null)
{
_logger.LogWarning("CRMService.GetNoteByCustomer - Note not found");
throw new RpcException(new Status(StatusCode.NotFound, $"Not Found Note for customer with ID {request.CustomerId}"));
}
NoteListResponse response = new NoteListResponse();
foreach(var n in noteList)
{
response.NoteList.Add(_mapper.Map<Note, rpcNote>(n));
}
return Task.FromResult(response);
}
public override async Task<NoteListResponse> GetAllNotes(EmptyReq request, ServerCallContext context)
{
LogCall();
var noteList = await _context.Notes.ToListAsync();
NoteListResponse response = new NoteListResponse();
foreach (var n in noteList)
{
response.NoteList.Add(_mapper.Map<Note, rpcNote>(n));
}
return response;
}
#endregion
public override async Task<rpcNote> AddNote(rpcAddNoteReq request, ServerCallContext context)
{
LogCall();
try
{
var noteToAdd = _mapper.Map<rpcAddNoteReq, Note>(request);
if (String.IsNullOrWhiteSpace(noteToAdd.CustomerNIF))
{
throw new RpcException(new Status(StatusCode.InvalidArgument, $"Invalid customer ID"));
}
noteToAdd.CreateTime = DateTime.UtcNow;
//No hacer async para el Add.
var item = _context.Notes.Add(noteToAdd);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.AddNote: Added {rows} customers");
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Note can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Note with ID: {noteToAdd.Id} added");
return _mapper.Map<Note, rpcNote>(item.Entity);
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error adding Customer: " + e.Message));
}
}
public override async Task<EmptyResponse> RemoveNote(NoteIdReq request, ServerCallContext context)
{
LogCall(); try
{
Note? n = await _context.Notes.FindAsync(request.NoteId);
if (n == null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Note ID: {request.NoteId} not fount"));
}
_context.Notes.Remove(n);
int rows = await _context.SaveChangesAsync();
if (rows < 1)
{
throw new RpcException(new Status(StatusCode.Internal, "Note can't be deleted"));
}
context.Status = new Status(StatusCode.OK, $"Note with ID: {request.NoteId} deleted");
return new EmptyResponse() { Msg = $"Note Deleted - ID {request.NoteId}" };
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Unknown, "Error removing Note: " + e.Message));
}
}
public override async Task<rpcNote> UpdateNote(rpcUpdateNoteReq request, ServerCallContext context)
{
LogCall();
try
{
//var updatedNote = _mapper.Map<rpcUpdateNoteReq, Note>(request);
//Check if ID already exists. Find podría ser Async...
var note = await _context.Notes.FindAsync(request.Id);
if (note is null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Note with ID {request.Id} Not Found"));
}
//Update Entity.
bool updated = false;
if (request.HasCustomerNIF && request.CustomerNIF != note.CustomerNIF) { note.CustomerNIF = request.CustomerNIF; updated = true; }
if (request.HasData && request.Data != note.Data) { note.Data = request.Data; updated = true; }
if (request.HasData && request.Status != note.Status) { note.Status = request.Status; updated = true; }
//No hacer async para el Add.
//var item = _context.Customers.Update(updatedCustomer);
int rows = await _context.SaveChangesAsync();
_logger.LogInformation($"CRMService.UpdateNote: Updated {rows} Note");
if (updated && rows < 1)
{
throw new RpcException(new Status(StatusCode.DataLoss, "Note can't be saved"));
}
context.Status = new Status(StatusCode.OK, $"Note with ID: {request.Id} updated");
return _mapper.Map<Note, rpcNote>(note);
}
catch (Exception e)
{
LogUnkwonErrors(e);
throw new RpcException(new Status(StatusCode.Aborted, "Error updating Note: " + e.Message));
}
}
#endregion
#region Utils.
private void LogCall([CallerMemberName] string callerName = "")
{
_logger.LogInformation($"Begin grpc call {GetType().FullName}.{callerName}:");
}
private void LogUnkwonErrors(Exception e, [CallerMemberName] string callerName = "")
{
_logger.LogError($"{GetType().FullName}.{callerName}: UnknowError -> Exception: {e.Message} - {e.StackTrace}");
}
#endregion
}
+8
View File
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}
+43
View File
@@ -0,0 +1,43 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33627.172
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CRM.API", "CRM.API\CRM.API.csproj", "{A8F7493B-C3B0-4CDD-8956-A9A8CC48A512}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{627DBAE1-551C-45AA-8BF8-2C8A66FED5E3}"
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{C53CC93C-36C0-4A7B-AAFE-F5A327EDEF38}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BookMng.API", "BookMng.API\BookMng.API.csproj", "{7FEB5B10-6385-4B35-A099-9E6B8B74F075}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A8F7493B-C3B0-4CDD-8956-A9A8CC48A512}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A8F7493B-C3B0-4CDD-8956-A9A8CC48A512}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A8F7493B-C3B0-4CDD-8956-A9A8CC48A512}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A8F7493B-C3B0-4CDD-8956-A9A8CC48A512}.Release|Any CPU.Build.0 = Release|Any CPU
{C53CC93C-36C0-4A7B-AAFE-F5A327EDEF38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C53CC93C-36C0-4A7B-AAFE-F5A327EDEF38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C53CC93C-36C0-4A7B-AAFE-F5A327EDEF38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C53CC93C-36C0-4A7B-AAFE-F5A327EDEF38}.Release|Any CPU.Build.0 = Release|Any CPU
{7FEB5B10-6385-4B35-A099-9E6B8B74F075}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7FEB5B10-6385-4B35-A099-9E6B8B74F075}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7FEB5B10-6385-4B35-A099-9E6B8B74F075}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7FEB5B10-6385-4B35-A099-9E6B8B74F075}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A8F7493B-C3B0-4CDD-8956-A9A8CC48A512} = {627DBAE1-551C-45AA-8BF8-2C8A66FED5E3}
{7FEB5B10-6385-4B35-A099-9E6B8B74F075} = {627DBAE1-551C-45AA-8BF8-2C8A66FED5E3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F5B82DFB-D123-41DE-9BEA-C6E13F805A3B}
EndGlobalSection
EndGlobal
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerTargetOS>Linux</DockerTargetOS>
<ProjectGuid>c53cc93c-36c0-4a7b-aafe-f5a327edef38</ProjectGuid>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">
<DependentUpon>docker-compose.yml</DependentUpon>
</None>
<None Include="docker-compose.yml" />
<None Include=".dockerignore" />
</ItemGroup>
</Project>
+25
View File
@@ -0,0 +1,25 @@
version: '3.4'
services:
crm.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
ports:
- "5105:80"
- "5106:443"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
bookmng.api:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
ports:
- "5107:80"
- "5108:443"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
+23
View File
@@ -0,0 +1,23 @@
version: '3.4'
services:
sqldb:
image: mariadb
crm.api:
image: ${DOCKER_REGISTRY-}crmapi
build:
context: .
dockerfile: CRM.API/Dockerfile
depends_on:
- sqldb
bookmng.api:
image: ${DOCKER_REGISTRY-}bookmngapi
build:
context: .
dockerfile: BookMng.API/Dockerfile
depends_on:
- sqldb
+12
View File
@@ -0,0 +1,12 @@
{
"profiles": {
"Docker Compose": {
"commandName": "DockerCompose",
"commandVersion": "1.0",
"serviceActions": {
"bookmng.api": "StartDebugging",
"crm.api": "StartDebugging"
}
}
}
}