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