diff --git a/.editorconfig b/.editorconfig
index 03f42ef..78f0805 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -97,7 +97,7 @@ dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggest
#Style - Modifier preferences
#when this rule is set to a list of modifiers, prefer the specified ordering.
-csharp_preferred_modifier_order = public,private,static,async,readonly:suggestion
+csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async:suggestion
#Style - Pattern matching
diff --git a/Authors.Grpc/Authors - Backup.Grpc.csproj b/Authors.Grpc/Authors - Backup.Grpc.csproj
new file mode 100644
index 0000000..f2dc639
--- /dev/null
+++ b/Authors.Grpc/Authors - Backup.Grpc.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ Both
+
+
+
+
diff --git a/Authors.Grpc/Authors.Grpc.csproj b/Authors.Grpc/Authors.Grpc.csproj
new file mode 100644
index 0000000..c80259f
--- /dev/null
+++ b/Authors.Grpc/Authors.Grpc.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/Authors.Grpc/authors.proto b/Authors.Grpc/authors.proto
new file mode 100644
index 0000000..f3387b1
--- /dev/null
+++ b/Authors.Grpc/authors.proto
@@ -0,0 +1,69 @@
+syntax = "proto3";
+
+option csharp_namespace="AuthorsService.Grpc";
+
+service AuthorsServiceProto {
+ rpc GetAuthors(GetAuthorsRequest) returns (GetAuthorsResponse);
+ rpc GetAuthor(GetAuthorRequest) returns (GetAuthorResponse);
+ rpc UpdateAuthor(UpdateAuthorRequest) returns (UpdateAuthorResponse);
+ rpc CreateAuthor(CrateAuthorRequest) returns (CrateAuthorResponse);
+ rpc DeleteAuthor(DeleteAuthorRequest) returns (DeleteAuthorResponse);
+ rpc UpdateAuthorBooksCount(UpdateAuthorBooksCountRequest) returns (UpdateAuthorBooksCountResponse);
+}
+
+message Author {
+ string id = 1;
+ string firstName = 2;
+ string lastName = 3;
+ uint32 age = 4;
+ string biography = 5;
+ uint32 numberOfBooks = 6;
+}
+
+message GetAuthorsRequest {};
+
+message GetAuthorsResponse {
+ repeated Author authors = 1;
+};
+
+message GetAuthorRequest {
+ string id = 1;
+};
+
+message GetAuthorResponse {
+ Author author = 1;
+};
+
+message UpdateAuthorRequest {
+ Author author = 1;
+};
+
+message UpdateAuthorResponse {};
+
+message CrateAuthorRequest {
+ Author author = 1;
+};
+
+message CrateAuthorResponse {
+ string id = 1;
+};
+
+message DeleteAuthorRequest {
+ string id = 1;
+}
+
+message DeleteAuthorResponse {}
+
+enum UpdateType {
+ None = 0;
+ Increase = 1;
+ Decrease = 2;
+}
+
+message UpdateAuthorBooksCountRequest {
+ string id = 1;
+ uint32 delta = 2;
+ UpdateType updateType = 3;
+}
+
+message UpdateAuthorBooksCountResponse {}
\ No newline at end of file
diff --git a/AuthorsService/AuthorsService.csproj b/AuthorsService/AuthorsService.csproj
index 5cfc683..33ac8f6 100644
--- a/AuthorsService/AuthorsService.csproj
+++ b/AuthorsService/AuthorsService.csproj
@@ -24,6 +24,7 @@
+
diff --git a/AuthorsService/Controllers/AuthorsController.cs b/AuthorsService/Controllers/AuthorsController.cs
index 7c1eb46..668b430 100644
--- a/AuthorsService/Controllers/AuthorsController.cs
+++ b/AuthorsService/Controllers/AuthorsController.cs
@@ -49,9 +49,9 @@ public async Task UpdateAuthor(Guid id, AuthorDto authorDto, Canc
if (author is null) return this.NotFound();
author.Age = authorDto.Age;
- author.Biography = author.Biography;
- author.FirstName = author.FirstName;
- author.LastName = author.LastName;
+ author.Biography = authorDto.Biography;
+ author.FirstName = authorDto.FirstName;
+ author.LastName = authorDto.LastName;
author.NumberOfBooks = authorDto.NumberOfBooks;
this.context.Entry(author).State = EntityState.Modified;
diff --git a/AuthorsService/Controllers/AuthorsGrpcService.cs b/AuthorsService/Controllers/AuthorsGrpcService.cs
new file mode 100644
index 0000000..d04c336
--- /dev/null
+++ b/AuthorsService/Controllers/AuthorsGrpcService.cs
@@ -0,0 +1,184 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+
+using AuthorsService.Extensions;
+using AuthorsService.Grpc;
+using AuthorsService.Models;
+
+using Grpc.Core;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+using Author = AuthorsService.Models.Author;
+
+namespace AuthorsService.Controllers
+{
+ public class AuthorsGrpcService : AuthorsServiceProto.AuthorsServiceProtoBase
+ {
+ private readonly AuthorContext context;
+ private readonly ILogger logger;
+
+ public AuthorsGrpcService(AuthorContext context, ILogger logger)
+ {
+ this.context = context;
+ this.logger = logger;
+ }
+
+ public override async Task GetAuthors(GetAuthorsRequest request,
+ ServerCallContext callContext)
+ {
+ try
+ {
+ var authors = await this.context.Authors.Select(a => a.ToGrpcDto())
+ .ToListAsync(callContext.CancellationToken);
+ GetAuthorsResponse response = new();
+ response.Authors.AddRange(authors);
+ return response;
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ throw new RpcException(new Status(StatusCode.Internal, e.Message));
+ }
+ }
+
+ public override async Task GetAuthor(GetAuthorRequest request, ServerCallContext callContext)
+ {
+ try
+ {
+ var author =
+ await this.context.Authors.FindAsync(new object[] {request.Id}, callContext.CancellationToken);
+
+ if (author == null)
+ throw new RpcException(new Status(StatusCode.NotFound, $"There is no author with id {request.Id}"));
+
+ GetAuthorResponse response = new() {Author = author.ToGrpcDto()};
+ return response;
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ throw new RpcException(new Status(StatusCode.Internal, e.Message));
+ }
+ }
+
+ public override async Task
+ UpdateAuthor(UpdateAuthorRequest request, ServerCallContext callContext)
+ {
+ var author =
+ await this.context.Authors.FindAsync(new object[] {request.Author.Id}, callContext.CancellationToken);
+
+ if (author is null)
+ {
+ throw new RpcException(new Status(StatusCode.NotFound,
+ $"There is no author with id {request.Author.Id}"));
+ }
+
+ author.Age = request.Author.Age;
+ author.Biography = request.Author.Biography;
+ author.FirstName = request.Author.FirstName;
+ author.LastName = request.Author.LastName;
+ author.NumberOfBooks = request.Author.NumberOfBooks;
+ this.context.Entry(author).State = EntityState.Modified;
+
+ try
+ {
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!this.context.Authors.Any(a => a.Id.ToString() == request.Author.Id))
+ {
+ throw new RpcException(new Status(StatusCode.NotFound,
+ $"There is no author with id {request.Author.Id}"));
+ }
+
+ throw new RpcException(new Status(StatusCode.Internal, "Couldn't update author"));
+ }
+
+ return new UpdateAuthorResponse();
+ }
+
+ public override async Task CreateAuthor(CrateAuthorRequest request,
+ ServerCallContext callContext)
+ {
+ var author = new Author {
+ Age = request.Author.Age,
+ Biography = request.Author.Biography,
+ FirstName = request.Author.FirstName,
+ Id = Guid.NewGuid(),
+ LastName = request.Author.LastName,
+ NumberOfBooks = request.Author.NumberOfBooks
+ };
+
+ try
+ {
+ this.context.Authors.Add(author);
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+ CrateAuthorResponse response = new() {Id = author.Id.ToString()};
+ return response;
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ throw new RpcException(new Status(StatusCode.Internal, e.Message));
+ }
+ }
+
+ public override async Task
+ DeleteAuthor(DeleteAuthorRequest request, ServerCallContext callContext)
+ {
+ var author = await this.context.Authors.FindAsync(new object[] {request.Id}, callContext.CancellationToken);
+ if (author == null)
+ throw new RpcException(new Status(StatusCode.NotFound, $"There is no author with id {request.Id}"));
+
+ this.context.Authors.Remove(author);
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+
+ return new DeleteAuthorResponse();
+ }
+
+ public override async Task UpdateAuthorBooksCount(
+ UpdateAuthorBooksCountRequest request, ServerCallContext callContext)
+ {
+ var author = await this.context.Authors.FindAsync(new object[] { request.Id }, callContext.CancellationToken);
+ if (author == null)
+ throw new RpcException(new Status(StatusCode.NotFound, $"There is no author with id {request.Id}"));
+
+ switch (request.UpdateType)
+ {
+ case UpdateType.Increase:
+ author.NumberOfBooks += request.Delta;
+ break;
+ case UpdateType.Decrease:
+ if (author.NumberOfBooks < request.Delta)
+ throw new RpcException(new Status(StatusCode.InvalidArgument, $"Author has {author.NumberOfBooks} books but delta is {request.Delta}"));
+ author.NumberOfBooks -= request.Delta;
+ break;
+ default:
+ throw new RpcException(new Status(StatusCode.InvalidArgument, $"Invalid update type {request.UpdateType}"));
+ }
+
+ this.context.Entry(author).State = EntityState.Modified;
+
+ try
+ {
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!this.context.Authors.Any(a => a.Id.ToString() == request.Id))
+ {
+ throw new RpcException(new Status(StatusCode.NotFound,
+ $"There is no author with id {request.Id}"));
+ }
+
+ throw new RpcException(new Status(StatusCode.Internal, "Couldn't update author"));
+ }
+
+ return new UpdateAuthorBooksCountResponse();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AuthorsService/Dockerfile b/AuthorsService/Dockerfile
index 0fd4194..df40965 100644
--- a/AuthorsService/Dockerfile
+++ b/AuthorsService/Dockerfile
@@ -2,8 +2,9 @@
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
-EXPOSE 80
-EXPOSE 443
+EXPOSE 5000
+EXPOSE 5001
+EXPOSE 5002
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
diff --git a/AuthorsService/Extensions/AuthorExtensions.cs b/AuthorsService/Extensions/AuthorExtensions.cs
index 851eb83..0f233a6 100644
--- a/AuthorsService/Extensions/AuthorExtensions.cs
+++ b/AuthorsService/Extensions/AuthorExtensions.cs
@@ -1,6 +1,9 @@
using AuthorsService.Models;
+
using SharedTypes.Api;
+using AuthorGrpcDto = AuthorsService.Grpc.Author;
+
namespace AuthorsService.Extensions
{
public static class AuthorExtensions
@@ -15,5 +18,17 @@ public static AuthorDto ToDto(this Author author) =>
LastName = author.LastName,
NumberOfBooks = author.NumberOfBooks
};
+
+ public static AuthorGrpcDto ToGrpcDto(this Author author) =>
+ new()
+ {
+ Age = author.Age,
+ Biography = author.Biography,
+ FirstName = author.FirstName,
+ Id = author.Id.ToString(),
+ LastName = author.LastName,
+ NumberOfBooks = author.NumberOfBooks
+ };
+
}
}
\ No newline at end of file
diff --git a/AuthorsService/Models/Author.cs b/AuthorsService/Models/Author.cs
index d1e2a68..9f8dfec 100644
--- a/AuthorsService/Models/Author.cs
+++ b/AuthorsService/Models/Author.cs
@@ -7,7 +7,7 @@ public class Author
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
- public byte Age { get; set; }
+ public uint Age { get; set; }
public string Biography { get; set; }
public uint NumberOfBooks { get; set; }
}
diff --git a/AuthorsService/Startup.cs b/AuthorsService/Startup.cs
index 7f251c7..ba1dc1d 100644
--- a/AuthorsService/Startup.cs
+++ b/AuthorsService/Startup.cs
@@ -1,3 +1,4 @@
+using AuthorsService.Controllers;
using AuthorsService.Models;
using Microsoft.AspNetCore.Builder;
@@ -29,6 +30,8 @@ public void ConfigureServices(IServiceCollection services)
{
c.SwaggerDoc("v1", new OpenApiInfo {Title = "AuthorsService", Version = "v1"});
});
+ services.AddLogging();
+ services.AddGrpc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -50,6 +53,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
+ endpoints.MapGrpcService();
});
}
}
diff --git a/AuthorsService/appsettings.Development.json b/AuthorsService/appsettings.Development.json
index 8983e0f..3e0d0a3 100644
--- a/AuthorsService/appsettings.Development.json
+++ b/AuthorsService/appsettings.Development.json
@@ -5,5 +5,21 @@
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
+ },
+ "Kestrel": {
+ "Endpoints": {
+ "WebApi": {
+ "Url": "http://+:5000",
+ "Protocols": "Http1AndHttp2"
+ },
+ "Management": {
+ "Url": "http://+:5001",
+ "Protocols": "Http1AndHttp2"
+ },
+ "Grpc": {
+ "Url": "http://+:5002",
+ "Protocols": "Http2"
+ }
+ }
}
}
diff --git a/Books.Grpc/Books.Grpc.csproj b/Books.Grpc/Books.Grpc.csproj
new file mode 100644
index 0000000..21dff27
--- /dev/null
+++ b/Books.Grpc/Books.Grpc.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+ Both
+
+
+
+
diff --git a/Books.Grpc/books.proto b/Books.Grpc/books.proto
new file mode 100644
index 0000000..332d063
--- /dev/null
+++ b/Books.Grpc/books.proto
@@ -0,0 +1,52 @@
+syntax = "proto3";
+
+option csharp_namespace="BooksService.Grpc";
+
+service BooksServiceProto {
+ rpc GetBooks(GetBooksRequest) returns (GetBooksResponse);
+ rpc GetBook(GetBookRequest) returns (GetBookResponse);
+ rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse);
+ rpc CreateBook(CrateBookRequest) returns (CrateBookResponse);
+ rpc DeleteBook(DeleteBookRequest) returns (DeleteBooksResponse);
+}
+
+message Book {
+ string id = 1;
+ string authorId = 2;
+ string title = 3;
+ string description = 4;
+}
+
+message GetBooksRequest {};
+
+message GetBooksResponse {
+ repeated Book books = 1;
+};
+
+message GetBookRequest {
+ string id = 1;
+};
+
+message GetBookResponse {
+ Book book = 1;
+};
+
+message UpdateBookRequest {
+ Book book = 1;
+};
+
+message UpdateBookResponse {};
+
+message CrateBookRequest {
+ Book book = 1;
+};
+
+message CrateBookResponse {
+ string id = 1;
+};
+
+message DeleteBookRequest {
+ string id = 1;
+}
+
+message DeleteBooksResponse {}
diff --git a/BooksService/BooksService.csproj b/BooksService/BooksService.csproj
index 8f0043c..8240464 100644
--- a/BooksService/BooksService.csproj
+++ b/BooksService/BooksService.csproj
@@ -20,6 +20,8 @@
+
+
diff --git a/BooksService/Controllers/BooksGrpcService.cs b/BooksService/Controllers/BooksGrpcService.cs
new file mode 100644
index 0000000..3d817d4
--- /dev/null
+++ b/BooksService/Controllers/BooksGrpcService.cs
@@ -0,0 +1,190 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using AuthorsService.Grpc;
+
+using BooksService.Extensions;
+using BooksService.Grpc;
+using BooksService.Models;
+
+using Grpc.Core;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
+
+using Book = BooksService.Models.Book;
+
+namespace BooksService.Controllers
+{
+ public class BooksGrpcService : BooksServiceProto.BooksServiceProtoBase
+ {
+ private readonly BookContext context;
+ private readonly AuthorsServiceProto.AuthorsServiceProtoClient authorsClient;
+ private readonly ILogger logger;
+
+ public BooksGrpcService(BookContext context, AuthorsServiceProto.AuthorsServiceProtoClient authorsClient,
+ ILogger logger)
+ {
+ this.context = context;
+ this.authorsClient = authorsClient;
+ this.logger = logger;
+ }
+
+ public override async Task GetBooks(GetBooksRequest request, ServerCallContext callContext)
+ {
+ try
+ {
+ var books = await this.context.Books.Select(a => a.ToGrpcDto())
+ .ToListAsync(callContext.CancellationToken);
+ GetBooksResponse response = new();
+ response.Books.AddRange(books);
+ return response;
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ throw new RpcException(new Status(StatusCode.Internal, e.Message));
+ }
+ }
+
+ public override async Task GetBook(GetBookRequest request, ServerCallContext callContext)
+ {
+ try
+ {
+ var book =
+ await this.context.Books.FindAsync(new object[] {request.Id}, callContext.CancellationToken);
+
+ if (book == null)
+ throw new RpcException(new Status(StatusCode.NotFound, $"There is no author with id {request.Id}"));
+
+ GetBookResponse response = new() {Book = book.ToGrpcDto()};
+ return response;
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ throw new RpcException(new Status(StatusCode.Internal, e.Message));
+ }
+ }
+
+ public override async Task UpdateBook(UpdateBookRequest request,
+ ServerCallContext callContext)
+ {
+ var book =
+ await this.context.Books.FindAsync(new object[] {request.Book.Id}, callContext.CancellationToken);
+
+ if (book is null)
+ {
+ throw new RpcException(new Status(StatusCode.NotFound,
+ $"There is no book with id {request.Book.Id}"));
+ }
+
+ book.AuthorId = Guid.Parse(request.Book.AuthorId);
+ book.Description = request.Book.Description;
+ book.Title = request.Book.Title;
+ this.context.Entry(book).State = EntityState.Modified;
+
+ try
+ {
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+ }
+ catch (DbUpdateConcurrencyException)
+ {
+ if (!this.context.Books.Any(a => a.Id.ToString() == request.Book.Id))
+ {
+ throw new RpcException(new Status(StatusCode.NotFound,
+ $"There is no book with id {request.Book.Id}"));
+ }
+
+ throw new RpcException(new Status(StatusCode.Internal, "Couldn't update book"));
+ }
+
+ return new UpdateBookResponse();
+ }
+
+ public override async Task CreateBook(CrateBookRequest request, ServerCallContext callContext)
+ {
+ await this.UpdateAuthorBooksCount(request.Book.AuthorId, UpdateType.Increase,
+ callContext.CancellationToken);
+
+ var book = new Book {
+ Description = request.Book.Description,
+ Id = Guid.NewGuid(),
+ Title = request.Book.Title,
+ AuthorId = Guid.Parse(request.Book.AuthorId)
+ };
+
+ try
+ {
+ this.context.Books.Add(book);
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+
+ return new CrateBookResponse {Id = book.Id.ToString()};
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ await this.UpdateAuthorBooksCount(request.Book.AuthorId, UpdateType.Decrease,
+ callContext.CancellationToken);
+ throw new RpcException(new Status(StatusCode.Internal, "Book creation failed"));
+ }
+ }
+
+ public override async Task DeleteBook(DeleteBookRequest request,
+ ServerCallContext callContext)
+ {
+ var book = await this.context.Books.FindAsync(new object[] {request.Id}, callContext.CancellationToken);
+ if (book == null)
+ throw new RpcException(new Status(StatusCode.NotFound, $"There is no book with id {request.Id}"));
+
+ await this.UpdateAuthorBooksCount(book.AuthorId.ToString(), UpdateType.Decrease,
+ callContext.CancellationToken);
+
+ try
+ {
+ this.context.Books.Remove(book);
+ await this.context.SaveChangesAsync(callContext.CancellationToken);
+
+ return new DeleteBooksResponse();
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ await this.UpdateAuthorBooksCount(book.AuthorId.ToString(), UpdateType.Increase,
+ callContext.CancellationToken);
+ throw new RpcException(new Status(StatusCode.Internal, $"Cannot delete book {book.Id}"));
+ }
+ }
+
+ private async Task UpdateAuthorBooksCount(string authorId, UpdateType updateType,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ await this.authorsClient.UpdateAuthorBooksCountAsync(
+ new UpdateAuthorBooksCountRequest {Delta = 1, Id = authorId, UpdateType = updateType},
+ cancellationToken: cancellationToken);
+ }
+ catch (RpcException e)
+ {
+ this.logger.LogError(e.Message);
+ switch (e.StatusCode)
+ {
+ case StatusCode.NotFound:
+ case StatusCode.InvalidArgument:
+ throw new RpcException(e.Status, e.Message);
+ default:
+ throw new RpcException(new Status(StatusCode.Internal,
+ "Something went wrong when updating author books count"));
+ }
+ }
+ catch (Exception e)
+ {
+ this.logger.LogError(e.Message);
+ throw new RpcException(new Status(StatusCode.Internal, e.Message));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/BooksService/Dockerfile b/BooksService/Dockerfile
index 1e4a222..8b96a89 100644
--- a/BooksService/Dockerfile
+++ b/BooksService/Dockerfile
@@ -2,8 +2,9 @@
FROM mcr.microsoft.com/dotnet/aspnet:5.0 AS base
WORKDIR /app
-EXPOSE 80
-EXPOSE 443
+EXPOSE 5000
+EXPOSE 5001
+EXPOSE 5002
FROM mcr.microsoft.com/dotnet/sdk:5.0 AS build
WORKDIR /src
diff --git a/BooksService/Extensions/BookExtensions.cs b/BooksService/Extensions/BookExtensions.cs
index e8fc311..892ff06 100644
--- a/BooksService/Extensions/BookExtensions.cs
+++ b/BooksService/Extensions/BookExtensions.cs
@@ -13,5 +13,14 @@ public static BookDto ToDto(this Book book) =>
Description = book.Description,
Id = book.Id
};
+
+ public static BooksService.Grpc.Book ToGrpcDto(this Book book) =>
+ new()
+ {
+ AuthorId = book.AuthorId.ToString(),
+ Title = book.Title,
+ Description = book.Description,
+ Id = book.Id.ToString()
+ };
}
}
\ No newline at end of file
diff --git a/BooksService/Startup.cs b/BooksService/Startup.cs
index 7fb481a..33a5695 100644
--- a/BooksService/Startup.cs
+++ b/BooksService/Startup.cs
@@ -1,7 +1,11 @@
using System;
+using AuthorsService.Grpc;
+
using BooksService.ApiClients;
+using BooksService.Controllers;
using BooksService.Models;
+
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
@@ -14,7 +18,8 @@ namespace BooksService
{
public class Startup
{
- private const string AuthorsApiConfigKey = "AuthorsApiUrl";
+ private const string AuthorsRestApiConfigKey = "AuthorsRestApiUrl";
+ private const string AuthorsGrpcApiConfigKey = "AuthorsGrpcApiUrl";
public Startup(IConfiguration configuration)
{
@@ -33,9 +38,15 @@ public void ConfigureServices(IServiceCollection services)
{
c.SwaggerDoc("v1", new OpenApiInfo {Title = "BooksService", Version = "v1"});
});
+ services.AddGrpc();
+
+ var authorsRestApiUrl = this.Configuration.GetValue(Startup.AuthorsRestApiConfigKey);
+ services.AddHttpClient(AuthorsApiClient.Name, client => client.BaseAddress = new Uri(authorsRestApiUrl));
+ services.AddSingleton();
- var authorsApiUrl = this.Configuration.GetValue(Startup.AuthorsApiConfigKey);
- services.AddHttpClient(AuthorsApiClient.Name, client => client.BaseAddress = new Uri(authorsApiUrl));
+ var authorsGrpcApiUrl = this.Configuration.GetValue(Startup.AuthorsGrpcApiConfigKey);
+ services.AddGrpcClient(options =>
+ options.Address = new Uri(authorsGrpcApiUrl));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@@ -54,7 +65,11 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseAuthorization();
- app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
+ app.UseEndpoints(endpoints =>
+ {
+ endpoints.MapControllers();
+ endpoints.MapGrpcService();
+ });
}
}
}
\ No newline at end of file
diff --git a/BooksService/appsettings.Development.json b/BooksService/appsettings.Development.json
index 9faed88..9ef657e 100644
--- a/BooksService/appsettings.Development.json
+++ b/BooksService/appsettings.Development.json
@@ -6,5 +6,22 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
- "BooksApiUrl": "http://booksservice:80/api/v1/books/"
+ "AuthorsRestApiUrl": "http://authorsservice:5000/api/v1/authors/",
+ "AuthorsGrpcApiUrl": "http://authorsservice:5002",
+ "Kestrel": {
+ "Endpoints": {
+ "WebApi": {
+ "Url": "http://+:5000",
+ "Protocols": "Http1AndHttp2"
+ },
+ "Management": {
+ "Url": "http://+:5001",
+ "Protocols": "Http1AndHttp2"
+ },
+ "Grpc": {
+ "Url": "http://+:5002",
+ "Protocols": "Http2"
+ }
+ }
+ }
}
diff --git a/DistributedSystemsHomework.sln b/DistributedSystemsHomework.sln
index 75ffd8b..42a71ed 100644
--- a/DistributedSystemsHomework.sln
+++ b/DistributedSystemsHomework.sln
@@ -21,6 +21,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedTypes.Api", "SharedTy
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FrontendService", "FrontendService\FrontendService.csproj", "{D960CDAC-B654-4F8A-915E-092BAF929F19}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Authors.Grpc", "Authors.Grpc\Authors.Grpc.csproj", "{D78E1968-8280-4846-9CF9-9CA0934ADA6C}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Books.Grpc", "Books.Grpc\Books.Grpc.csproj", "{D459E437-A4C3-4F7C-BABA-88A29F0B152C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -47,6 +51,14 @@ Global
{D960CDAC-B654-4F8A-915E-092BAF929F19}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D960CDAC-B654-4F8A-915E-092BAF929F19}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D960CDAC-B654-4F8A-915E-092BAF929F19}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D78E1968-8280-4846-9CF9-9CA0934ADA6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D78E1968-8280-4846-9CF9-9CA0934ADA6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D78E1968-8280-4846-9CF9-9CA0934ADA6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D78E1968-8280-4846-9CF9-9CA0934ADA6C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D459E437-A4C3-4F7C-BABA-88A29F0B152C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D459E437-A4C3-4F7C-BABA-88A29F0B152C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D459E437-A4C3-4F7C-BABA-88A29F0B152C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D459E437-A4C3-4F7C-BABA-88A29F0B152C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/FrontendService/Contracts/Author.cs b/FrontendService/Contracts/Author.cs
index 8d87f16..6e47ece 100644
--- a/FrontendService/Contracts/Author.cs
+++ b/FrontendService/Contracts/Author.cs
@@ -7,7 +7,7 @@ public class Author
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
- public byte Age { get; set; }
+ public uint Age { get; set; }
public string Biography { get; set; }
public uint NumberOfBooks { get; set; }
}
diff --git a/FrontendService/Contracts/CreateAuthor.cs b/FrontendService/Contracts/CreateAuthor.cs
index 4cb4528..ce2a37a 100644
--- a/FrontendService/Contracts/CreateAuthor.cs
+++ b/FrontendService/Contracts/CreateAuthor.cs
@@ -4,7 +4,7 @@ public class CreateAuthor
{
public string FirstName { get; set; }
public string LastName { get; set; }
- public byte Age { get; set; }
+ public uint Age { get; set; }
public string Biography { get; set; }
}
}
\ No newline at end of file
diff --git a/FrontendService/Controllers/AuthorsControllerGrpc.cs b/FrontendService/Controllers/AuthorsControllerGrpc.cs
new file mode 100644
index 0000000..28f967b
--- /dev/null
+++ b/FrontendService/Controllers/AuthorsControllerGrpc.cs
@@ -0,0 +1,56 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using AuthorsService.Grpc;
+
+using FrontendService.Contracts;
+using FrontendService.Extensions;
+
+using Microsoft.AspNetCore.Mvc;
+
+using Author = FrontendService.Contracts.Author;
+
+// For more information on enabling Web API for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
+
+namespace FrontendService.Controllers
+{
+ [Route("api/v2/[controller]")]
+ [ApiController]
+ public class AuthorsControllerGrpc : ControllerBase
+ {
+ private readonly AuthorsServiceProto.AuthorsServiceProtoClient authorsApiClient;
+
+ public AuthorsControllerGrpc(AuthorsServiceProto.AuthorsServiceProtoClient authorsApiClient)
+ {
+ this.authorsApiClient = authorsApiClient;
+ }
+
+ [HttpGet]
+ public async Task> Get(CancellationToken cancellationToken) =>
+ (await this.authorsApiClient.GetAuthorsAsync(new GetAuthorsRequest(), cancellationToken: cancellationToken))
+ .Authors.Select(dto => dto.ToContract());
+
+ [HttpGet("{id}")]
+ public async Task GetAuthor(Guid id, CancellationToken cancellationToken) =>
+ (await this.authorsApiClient.GetAuthorAsync(new GetAuthorRequest {Id = id.ToString()},
+ cancellationToken: cancellationToken)).Author.ToContract();
+
+ [HttpPost]
+ public async Task CreateAuthor(CreateAuthor message, CancellationToken cancellationToken) =>
+ Guid.Parse((await this.authorsApiClient.CreateAuthorAsync(
+ new CrateAuthorRequest {
+ Author = new AuthorsService.Grpc.Author {
+ NumberOfBooks = 0,
+ Id = "",
+ Age = message.Age,
+ Biography = message.Biography,
+ FirstName = message.FirstName,
+ LastName = message.LastName
+ }
+ },
+ cancellationToken: cancellationToken)).Id);
+ }
+}
\ No newline at end of file
diff --git a/FrontendService/Controllers/BooksController.cs b/FrontendService/Controllers/BooksController.cs
index f402c54..4fbc301 100644
--- a/FrontendService/Controllers/BooksController.cs
+++ b/FrontendService/Controllers/BooksController.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Net;
using System.Threading;
using System.Threading.Tasks;
@@ -15,7 +14,7 @@
namespace FrontendService.Controllers
{
- [Route("[controller]")]
+ [Route("api/[controller]")]
[ApiController]
public class BooksController : ControllerBase
{
diff --git a/FrontendService/Controllers/BooksControllerGrpc.cs b/FrontendService/Controllers/BooksControllerGrpc.cs
new file mode 100644
index 0000000..854524c
--- /dev/null
+++ b/FrontendService/Controllers/BooksControllerGrpc.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+using AuthorsService.Grpc;
+
+using BooksService.Grpc;
+
+using FrontendService.ApiClients;
+using FrontendService.Contracts;
+using FrontendService.Extensions;
+
+using Microsoft.AspNetCore.Mvc;
+
+using SharedTypes.Api;
+
+using Book = FrontendService.Contracts.Book;
+
+namespace FrontendService.Controllers
+{
+ [Route("api/v2/[controller]")]
+ [ApiController]
+ public class BooksControllerGrpc : ControllerBase
+ {
+ private readonly AuthorsServiceProto.AuthorsServiceProtoClient authorsApiClient;
+ private readonly BooksServiceProto.BooksServiceProtoClient booksApiClient;
+
+ public BooksControllerGrpc(AuthorsServiceProto.AuthorsServiceProtoClient authorsApiClient,
+ BooksServiceProto.BooksServiceProtoClient booksApiClient)
+ {
+ this.authorsApiClient = authorsApiClient;
+ this.booksApiClient = booksApiClient;
+ }
+
+ [HttpGet]
+ public async Task> GetBooks(CancellationToken cancellationToken)
+ {
+ var response = await this.booksApiClient.GetBooksAsync(new GetBooksRequest(), cancellationToken: cancellationToken);
+ if (response.Books.Capacity == 0) return Array.Empty();
+
+ var authorDtos = (await this.authorsApiClient.GetAuthorsAsync(new GetAuthorsRequest(), cancellationToken: cancellationToken)).Authors.ToDictionary(a => a.Id);
+ return response.Books.Select(bookDto => bookDto.ToContract(authorDtos[bookDto.AuthorId]));
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetBook(Guid id, CancellationToken cancellationToken)
+ {
+ var bookResponse = await this.booksApiClient.GetBookAsync(new GetBookRequest {Id = id.ToString()}, cancellationToken: cancellationToken);
+ var authorResponse = await this.authorsApiClient.GetAuthorAsync(new GetAuthorRequest {Id = bookResponse.Book.AuthorId }, cancellationToken: cancellationToken);
+ return bookResponse.Book.ToContract(authorResponse.Author);
+ }
+
+ [HttpPost]
+ public async Task CreateBook(CreateBook message, CancellationToken cancellationToken)
+ {
+ var book = await this.booksApiClient.CreateBookAsync(
+ new CrateBookRequest {
+ Book = new BooksService.Grpc.Book {
+ AuthorId = message.AuthorId.ToString(),
+ Description = message.Description,
+ Id = "",
+ Title = message.Title
+ }
+ }, cancellationToken: cancellationToken);
+ return this.Ok(book.Id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/FrontendService/Extensions/AuthorExtensions.cs b/FrontendService/Extensions/AuthorExtensions.cs
index dca68aa..b9d1a4a 100644
--- a/FrontendService/Extensions/AuthorExtensions.cs
+++ b/FrontendService/Extensions/AuthorExtensions.cs
@@ -1,4 +1,6 @@
-using FrontendService.Contracts;
+using System;
+
+using FrontendService.Contracts;
using SharedTypes.Api;
@@ -14,5 +16,15 @@ public static class AuthorExtensions
FirstName = dto.FirstName,
Id = dto.Id
};
+
+ public static Author ToContract(this AuthorsService.Grpc.Author dto) => new()
+ {
+ NumberOfBooks = dto.NumberOfBooks,
+ LastName = dto.LastName,
+ Age = dto.Age,
+ Biography = dto.Biography,
+ FirstName = dto.FirstName,
+ Id = Guid.Parse(dto.Id)
+ };
}
}
\ No newline at end of file
diff --git a/FrontendService/Extensions/BooksExtensions.cs b/FrontendService/Extensions/BooksExtensions.cs
index d1cda07..9322d73 100644
--- a/FrontendService/Extensions/BooksExtensions.cs
+++ b/FrontendService/Extensions/BooksExtensions.cs
@@ -1,4 +1,6 @@
-using FrontendService.Contracts;
+using System;
+
+using FrontendService.Contracts;
using SharedTypes.Api;
@@ -14,5 +16,15 @@ public static class BooksExtensions
Id = bookDto.Id,
Title = bookDto.Title
};
+
+ public static Book ToContract(this BooksService.Grpc.Book bookDto, AuthorsService.Grpc.Author authorDto) => new()
+ {
+ AuthorFirstname = authorDto.FirstName,
+ AuthorId = Guid.Parse(bookDto.AuthorId),
+ AuthorLastName = authorDto.LastName,
+ Description = bookDto.Description,
+ Id = Guid.Parse(bookDto.Id),
+ Title = bookDto.Title
+ };
}
}
\ No newline at end of file
diff --git a/FrontendService/FrontendService.csproj b/FrontendService/FrontendService.csproj
index 944fcc6..498b15b 100644
--- a/FrontendService/FrontendService.csproj
+++ b/FrontendService/FrontendService.csproj
@@ -17,6 +17,8 @@
+
+
diff --git a/FrontendService/Startup.cs b/FrontendService/Startup.cs
index 75c07e6..33b192a 100644
--- a/FrontendService/Startup.cs
+++ b/FrontendService/Startup.cs
@@ -1,5 +1,9 @@
using System;
+using AuthorsService.Grpc;
+
+using BooksService.Grpc;
+
using FrontendService.ApiClients;
using Microsoft.AspNetCore.Builder;
@@ -14,7 +18,9 @@ namespace FrontendService
public class Startup
{
private const string BooksApiUrlConfigKey = "BooksApiUrl";
+ private const string BooksGrpcApiUrlConfigKey = "BooksGrpcApiUrl";
private const string AuthorsApiConfigKey = "AuthorsApiUrl";
+ private const string AuthorsGrpcApiConfigKey = "AuthorsGrpcApiUrl";
public Startup(IConfiguration configuration)
{
@@ -42,6 +48,14 @@ public void ConfigureServices(IServiceCollection services)
services.AddLogging();
services.AddSingleton();
services.AddSingleton();
+
+ var authorsGrpcApiUrl = this.Configuration.GetValue(Startup.AuthorsGrpcApiConfigKey);
+ services.AddGrpcClient(options =>
+ options.Address = new Uri(authorsGrpcApiUrl));
+
+ var booksGrpcApiUrl = this.Configuration.GetValue(Startup.BooksGrpcApiUrlConfigKey);
+ services.AddGrpcClient(options =>
+ options.Address = new Uri(booksGrpcApiUrl));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
diff --git a/FrontendService/appsettings.Development.json b/FrontendService/appsettings.Development.json
index f32288e..bdf7452 100644
--- a/FrontendService/appsettings.Development.json
+++ b/FrontendService/appsettings.Development.json
@@ -6,6 +6,8 @@
"Microsoft.Hosting.Lifetime": "Information"
}
},
- "BooksApiUrl": "http://booksservice:80/api/v1/books/",
- "AuthorsApiUrl": "http://authorsservice:80/api/v1/authors/"
-}
+ "BooksApiUrl": "http://booksservice:5000/api/v1/books/",
+ "BooksGrpcApiUrl": "http://booksservice:5002",
+ "AuthorsApiUrl": "http://authorsservice:5000/api/v1/authors/",
+ "AuthorsGrpcApiUrl": "http://authorsservice:5002"
+}
\ No newline at end of file
diff --git a/Shared.Proto/Shared.Proto.csproj b/Shared.Proto/Shared.Proto.csproj
new file mode 100644
index 0000000..88a4d28
--- /dev/null
+++ b/Shared.Proto/Shared.Proto.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net5.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
diff --git a/Shared.Proto/authors.proto b/Shared.Proto/authors.proto
new file mode 100644
index 0000000..6a564a9
--- /dev/null
+++ b/Shared.Proto/authors.proto
@@ -0,0 +1,69 @@
+syntax = "proto3";
+
+option csharp_namespace="AutorsService.Grpc";
+
+service AuthorsServiceProto {
+ rpc GetAuthors(GetAuthorsRequest) returns (GetAuthorsResponse);
+ rpc GetAuthor(GetAuthorRequest) returns (GetAuthorResponse);
+ rpc UpdateAuthor(UpdateAuthorRequest) returns (UpdateAuthorResponse);
+ rpc CrateAuthor(CrateAuthorRequest) returns (CrateAuthorResponse);
+ rpc DeleteAutor(DeleteAuthorRequest) returns (DeleteAuthorResponse);
+ rpc UpdateAuthorBooksCount(UpdateAuthorBooksCountRequest) returns (UpdateAuthorBooksCountResponse);
+}
+
+message Author {
+ string id = 1;
+ string firstName = 2;
+ string lastName = 3;
+ uint32 age = 4;
+ string biography = 5;
+ uint32 numberOfBooks = 6;
+}
+
+message GetAuthorsRequest {};
+
+message GetAuthorsResponse {
+ repeated Author authors = 1;
+};
+
+message GetAuthorRequest {
+ string id = 1;
+};
+
+message GetAuthorResponse {
+ Author author = 1;
+};
+
+message UpdateAuthorRequest {
+ Author author = 1;
+};
+
+message UpdateAuthorResponse {};
+
+message CrateAuthorRequest {
+ Author author = 1;
+};
+
+message CrateAuthorResponse {
+ Author author = 1;
+};
+
+message DeleteAuthorRequest {
+ string id = 1;
+}
+
+message DeleteAuthorResponse {}
+
+enum UpdateType {
+ None = 0;
+ Increase = 1;
+ Decrease = 2;
+}
+
+message UpdateAuthorBooksCountRequest {
+ string id = 1;
+ uint32 delta = 2;
+ UpdateType updateType = 3;
+}
+
+message UpdateAuthorBooksCountResponse {}
\ No newline at end of file
diff --git a/Shared.Proto/books.proto b/Shared.Proto/books.proto
new file mode 100644
index 0000000..47055ce
--- /dev/null
+++ b/Shared.Proto/books.proto
@@ -0,0 +1,52 @@
+syntax = "proto3";
+
+option csharp_namespace="BooksService.Grpc";
+
+service BooksServiceProto {
+ rpc GetBooks(GetBooksRequest) returns (GetBooksResponse);
+ rpc GetBook(GetBookRequest) returns (GetBookResponse);
+ rpc UpdateBook(UpdateBookRequest) returns (UpdateBookResponse);
+ rpc CrateBook(CrateBookRequest) returns (CrateBookResponse);
+ rpc DeleteBook(DeleteBookRequest) returns (DeleteBooksResponse);
+}
+
+message Book {
+ string id = 1;
+ string authorId = 2;
+ string title = 3;
+ string description = 4;
+}
+
+message GetBooksRequest {};
+
+message GetBooksResponse {
+ repeated Book books = 1;
+};
+
+message GetBookRequest {
+ string id = 1;
+};
+
+message GetBookResponse {
+ Book book = 1;
+};
+
+message UpdateBookRequest {
+ Book book = 1;
+};
+
+message UpdateBookResponse {};
+
+message CrateBookRequest {
+ Book book = 1;
+};
+
+message CrateBookResponse {
+ Book book = 1;
+};
+
+message DeleteBookRequest {
+ string id = 1;
+}
+
+message DeleteBooksResponse {}
diff --git a/SharedTypes.Api/AuthorDto.cs b/SharedTypes.Api/AuthorDto.cs
index d3edaaa..6af47fa 100644
--- a/SharedTypes.Api/AuthorDto.cs
+++ b/SharedTypes.Api/AuthorDto.cs
@@ -7,7 +7,7 @@ public class AuthorDto
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
- public byte Age { get; set; }
+ public uint Age { get; set; }
public string Biography { get; set; }
public uint NumberOfBooks { get; set; }
}
diff --git a/docker-compose.override.yml b/docker-compose.override.yml
index 8fb8248..a3bfadf 100644
--- a/docker-compose.override.yml
+++ b/docker-compose.override.yml
@@ -6,8 +6,9 @@ services:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
ports:
- - "9080:80"
- - "9043:443"
+ - "9080:5000"
+ - "9081:5001"
+ - "9082:5003"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro
@@ -16,8 +17,9 @@ services:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
ports:
- - "9180:80"
- - "9143:443"
+ - "9180:5000"
+ - "9181:5001"
+ - "9182:5003"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro