├── BlazorAppDemo ├── wwwroot │ ├── favicon.png │ └── app.css ├── Components │ ├── Pages │ │ ├── Home.razor │ │ ├── Error.razor │ │ └── Weather.razor │ ├── Routes.razor │ ├── _Imports.razor │ ├── Layout │ │ ├── MainLayout.razor │ │ ├── NavMenu.razor │ │ ├── MainLayout.razor.css │ │ └── NavMenu.razor.css │ └── App.razor ├── appsettings.Development.json ├── appsettings.json ├── BlazorAppDemo.csproj ├── Program.cs └── Properties │ └── launchSettings.json ├── BlazorGrpc ├── BlazorGrpc │ ├── products.db │ ├── wwwroot │ │ ├── favicon.png │ │ └── app.css │ ├── appsettings.Development.json │ ├── appsettings.json │ ├── Components │ │ ├── Layout │ │ │ ├── NavMenu.razor │ │ │ ├── MainLayout.razor │ │ │ ├── MainLayout.razor.css │ │ │ └── NavMenu.razor.css │ │ ├── Routes.razor │ │ ├── _Imports.razor │ │ ├── ConfirmDialog.razor.cs │ │ ├── App.razor │ │ ├── ConfirmDialog.razor │ │ └── Pages │ │ │ ├── Error.razor │ │ │ ├── ProductAdd.razor.cs │ │ │ ├── ProductAdd.razor │ │ │ ├── ProductEdit.razor │ │ │ ├── ProductEdit.razor.cs │ │ │ ├── Weather.razor │ │ │ ├── Home.razor │ │ │ └── Home.razor.cs │ ├── Notification │ │ ├── ProductCreatedNotification.cs │ │ └── ProductCreatedHandler.cs │ ├── Validation │ │ └── ProductValidator.cs │ ├── Dialogs │ │ ├── ConfirmationDialog.razor.cs │ │ ├── ConfirmationDialog.razor │ │ ├── DeleteConfirmation.razor │ │ └── DeleteConfirmation.razor.cs │ ├── Handler │ │ ├── GetAllProductHandler.cs │ │ ├── GetByIdProductHandler.cs │ │ ├── DeleteByIdProductHandler.cs │ │ ├── UpdateProductHandler.cs │ │ └── CreateOrderHandler.cs │ ├── productService.proto │ ├── Properties │ │ └── launchSettings.json │ ├── UIServices │ │ └── DialogUIService.cs │ ├── BlazorGrpc.csproj │ ├── Service │ │ └── ServerProductService.cs │ └── Program.cs └── BlazorGrpc.Client │ ├── wwwroot │ ├── appsettings.json │ └── appsettings.Development.json │ ├── _Imports.razor │ ├── BlazorGrpc.Client.csproj │ ├── Pages │ └── Counter.razor │ └── Program.cs ├── BlazorGrpcSimpleMediater ├── IRequest.cs ├── INotification.cs ├── IHandler.cs ├── INotificationHandler.cs ├── IMediator.cs ├── BlazorGrpcSimpleMediater.csproj └── Mediator.cs ├── BlazorGrpc.FluentValidation ├── IValidator.cs ├── BlazorGrpc.FluentValidation.csproj ├── ValidationResult.cs ├── Validator.cs └── PropertyRule.cs ├── BlazorGrpc.Shared ├── ProductDto.cs └── BlazorGrpc.Shared.csproj ├── BlazorAppData ├── Interrface │ ├── UnitOfWork.cs │ └── IProductRepository.cs ├── DbContext │ └── ProductContext.cs ├── Migrations │ ├── 20240729093035_InitialCreate.cs │ ├── ProductContextModelSnapshot.cs │ └── 20240729093035_InitialCreate.Designer.cs ├── BlazorAppData.csproj ├── UnitOfWork │ └── UnitOfWork.cs ├── Model │ └── Product.cs └── Repository │ └── ProductRepository.cs ├── SECURITY.md ├── LICENSE ├── .gitattributes ├── BlazorGrpc.sln ├── README.md ├── CODE_OF_CONDUCT.md └── .gitignore /BlazorAppDemo/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevsharp/BlazorGrpcApp/HEAD/BlazorAppDemo/wwwroot/favicon.png -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/products.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevsharp/BlazorGrpcApp/HEAD/BlazorGrpc/BlazorGrpc/products.db -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/IRequest.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorGrpcSimpleMediater; 2 | 3 | public interface IRequest { } 4 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stevsharp/BlazorGrpcApp/HEAD/BlazorGrpc/BlazorGrpc/wwwroot/favicon.png -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | 3 | Home 4 | 5 |

Hello, world!

6 | 7 | Welcome to your new app. 8 | -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/INotification.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace BlazorGrpcSimpleMediater; 4 | 5 | public interface INotification 6 | { 7 | 8 | 9 | } 10 | -------------------------------------------------------------------------------- /BlazorAppDemo/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorGrpc.FluentValidation/IValidator.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace BlazorGrpc.FluentValidation; 3 | 4 | public interface IValidator 5 | { 6 | ValidationResult Validate(T instance); 7 | } 8 | 9 | 10 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc.Client/wwwroot/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorAppDemo/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc.Client/wwwroot/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/IHandler.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorGrpcSimpleMediater; 2 | 3 | public interface IHandler where TRequest : IRequest 4 | { 5 | Task Handle(TRequest request); 6 | } 7 | -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/INotificationHandler.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace BlazorGrpcSimpleMediater; 4 | 5 | public interface INotificationHandler where TNotification : INotification 6 | { 7 | Task Publish(TNotification notification); 8 | } 9 | -------------------------------------------------------------------------------- /BlazorGrpc.Shared/ProductDto.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorGrpc.Shared; 2 | 3 | public record ProductDto 4 | { 5 | public required int Id { get; set; } 6 | 7 | public required string Name { get; set; } 8 | 9 | public required decimal Price { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/IMediator.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorGrpcSimpleMediater; 2 | 3 | public interface IMediator 4 | { 5 | Task Send(IRequest request); 6 | 7 | Task PublishNotification(TEvent @event) where TEvent : INotification; 8 | } -------------------------------------------------------------------------------- /BlazorAppDemo/BlazorAppDemo.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BlazorGrpc.Shared/BlazorGrpc.Shared.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Routes.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /BlazorGrpc.FluentValidation/BlazorGrpc.FluentValidation.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | @using MudBlazor 2 | 3 | 4 | Home 5 | Add Product 6 | 7 | -------------------------------------------------------------------------------- /BlazorAppData/Interrface/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | namespace BlazorAppData.Interrface; 4 | 5 | public interface IUnitOfWork : IDisposable 6 | { 7 | Task Commit(CancellationToken cancellationToken); 8 | 9 | Task CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys); 10 | 11 | Task Rollback(); 12 | } 13 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Notification/ProductCreatedNotification.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpcSimpleMediater; 2 | 3 | namespace BlazorGrpc.Notification 4 | { 5 | public class ProductCreatedNotification : INotification 6 | { 7 | public string Message { get; } 8 | 9 | public ProductCreatedNotification(string message) => Message = message; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Routes.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/BlazorGrpcSimpleMediater.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc.Client/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorGrpc.Client 10 | @using MudBlazor -------------------------------------------------------------------------------- /BlazorAppDemo/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorAppDemo 10 | @using BlazorAppDemo.Components 11 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Notification/ProductCreatedHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpcSimpleMediater; 2 | 3 | namespace BlazorGrpc.Notification; 4 | 5 | public class ProducCreatedNotificationHandler : INotificationHandler 6 | { 7 | public Task Publish(ProductCreatedNotification notification) 8 | { 9 | Console.WriteLine($"Event received: {notification.Message}"); 10 | 11 | return Task.CompletedTask; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 |
4 | 7 | 8 |
9 |
10 | About 11 |
12 | 13 |
14 | @Body 15 |
16 |
17 |
18 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 7 | @using Microsoft.AspNetCore.Components.Web.Virtualization 8 | @using Microsoft.JSInterop 9 | @using BlazorGrpc 10 | @using BlazorGrpc.Client 11 | @using BlazorGrpc.Components 12 | @using MudBlazor 13 | -------------------------------------------------------------------------------- /BlazorAppData/Interrface/IProductRepository.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using BlazorGrpc.Model; 4 | 5 | namespace BlazorAppData.Interrface; 6 | 7 | public interface IProductRepository 8 | { 9 | Task> GetProductsAsync(); 10 | 11 | Task GetProductByIdAsync(object id); 12 | Task DeleteProductAsync(Product product); 13 | 14 | Task DeleteProductAsync(int productId); 15 | 16 | Task UpdateProductAsync(Product product); 17 | 18 | Task CreateProdutAsync(Product product); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Validation/ProductValidator.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpc.FluentValidation; 2 | using BlazorGrpc.Model; 3 | 4 | namespace BlazorGrpc.Validation; 5 | 6 | public class ProductValidator : Validator 7 | { 8 | public ProductValidator() 9 | { 10 | RuleFor(p => p.Name.Value, nameof(Product.Name.Value)) 11 | .NotEmpty() 12 | .Length(1, 10); 13 | 14 | RuleFor(p => p.Price.Value, nameof(Product.Price)).AddRule(p => p.Price.Value > 0m, "Price must be greater than 0."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Dialogs/ConfirmationDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using MudBlazor; 3 | 4 | namespace BlazorGrpc.Dialogs; 5 | 6 | public partial class ConfirmationDialog 7 | { 8 | [CascadingParameter] 9 | private MudDialogInstance MudDialog { get; set; } = default!; 10 | 11 | [Parameter] 12 | public string? ContentText { get; set; } 13 | 14 | private void Submit() 15 | { 16 | MudDialog.Close(DialogResult.Ok(true)); 17 | } 18 | 19 | private void Cancel() 20 | { 21 | MudDialog.Cancel(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Dialogs/ConfirmationDialog.razor: -------------------------------------------------------------------------------- 1 | @using MudBlazor 2 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 3 | @rendermode InteractiveServer 4 | 5 | 6 | 7 | @ContentText 8 | 9 | 10 | Cancel 11 | Confirm 12 | 13 | 14 | -------------------------------------------------------------------------------- /BlazorGrpc.FluentValidation/ValidationResult.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorGrpc.FluentValidation 2 | { 3 | public class ValidationResult 4 | { 5 | private readonly List _errors = []; 6 | 7 | public bool IsValid => !_errors.Any(); 8 | 9 | public IEnumerable Errors => _errors; 10 | 11 | public void AddError(string error) 12 | { 13 | _errors.Add(error); 14 | } 15 | 16 | public void AddErrors(IEnumerable errors) 17 | { 18 | _errors.AddRange(errors); 19 | } 20 | } 21 | } 22 | 23 | 24 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc.Client/BlazorGrpc.Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | Default 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 5.1.x | :white_check_mark: | 11 | | 5.0.x | :x: | 12 | | 4.0.x | :white_check_mark: | 13 | | < 4.0 | :x: | 14 | 15 | ## Reporting a Vulnerability 16 | 17 | Use this section to tell people how to report a vulnerability. 18 | 19 | Tell them where to go, how often they can expect to get an update on a 20 | reported vulnerability, what to expect if the vulnerability is accepted or 21 | declined, etc. 22 | -------------------------------------------------------------------------------- /BlazorAppDemo/Program.cs: -------------------------------------------------------------------------------- 1 | using BlazorAppDemo.Components; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | 5 | // Add services to the container. 6 | builder.Services.AddRazorComponents(); 7 | 8 | var app = builder.Build(); 9 | 10 | // Configure the HTTP request pipeline. 11 | if (!app.Environment.IsDevelopment()) 12 | { 13 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 14 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 15 | app.UseHsts(); 16 | } 17 | 18 | app.UseHttpsRedirection(); 19 | 20 | app.UseStaticFiles(); 21 | app.UseAntiforgery(); 22 | 23 | app.MapRazorComponents(); 24 | 25 | app.Run(); 26 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc.Client/Pages/Counter.razor: -------------------------------------------------------------------------------- 1 | @page "/counter" 2 | 3 | 4 | Counter 5 | 6 |

Counter

7 | 8 |

Current count: @currentCount

9 | 10 | 11 | 12 | 13 | @inject ISnackbar Snackbar 14 | 15 |

ProductAdd

16 | 17 | { config.ShowCloseIcon = false; }))" Variant="Variant.Filled" Color="Color.Primary"> 18 | Open Modified Snackbar 19 | 20 | 21 | @code { 22 | private int currentCount = 0; 23 | 24 | private void IncrementCount() 25 | { 26 | currentCount++; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Dialogs/DeleteConfirmation.razor: -------------------------------------------------------------------------------- 1 | @using MudBlazor 2 | @using static Microsoft.AspNetCore.Components.Web.RenderMode 3 | @rendermode InteractiveServer 4 | 5 | 6 | 7 | 8 | 9 | Delete 10 | 11 | 12 | 13 | @ContentText 14 | 15 | 16 | Cancel 17 | Confirm 18 | 19 | 20 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc.Client/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting; 2 | 3 | using MudBlazor; 4 | using MudBlazor.Services; 5 | 6 | var builder = WebAssemblyHostBuilder.CreateDefault(args); 7 | 8 | builder.Services.AddMudServices(config => 9 | { 10 | config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; 11 | 12 | config.SnackbarConfiguration.PreventDuplicates = false; 13 | config.SnackbarConfiguration.NewestOnTop = false; 14 | config.SnackbarConfiguration.ShowCloseIcon = true; 15 | config.SnackbarConfiguration.VisibleStateDuration = 10000; 16 | config.SnackbarConfiguration.HideTransitionDuration = 500; 17 | config.SnackbarConfiguration.ShowTransitionDuration = 500; 18 | config.SnackbarConfiguration.SnackbarVariant = Variant.Filled; 19 | }); 20 | 21 | 22 | await builder.Build().RunAsync(); 23 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Dialogs/DeleteConfirmation.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using MudBlazor; 3 | 4 | namespace BlazorGrpc.Dialogs; 5 | 6 | public interface IFormDialog 7 | { 8 | Func Refresh { get; set; } 9 | TCommand Model { get; set; } 10 | } 11 | 12 | public partial class DeleteConfirmation 13 | { 14 | [CascadingParameter] 15 | private MudDialogInstance MudDialog { get; set; } = default!; 16 | 17 | [EditorRequired] 18 | [Parameter] 19 | public string? ContentText { get; set; } 20 | 21 | [EditorRequired] 22 | [Parameter] 23 | public object Command { get; set; } = default!; 24 | private Task Submit() 25 | { 26 | MudDialog.Close(DialogResult.Ok(true)); 27 | 28 | return Task.CompletedTask; 29 | } 30 | 31 | private void Cancel() 32 | { 33 | MudDialog.Cancel(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @using MudBlazor 2 | @inherits LayoutComponentBase 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | @Body 20 | 21 | 22 | 23 | 24 | @code { 25 | bool _drawerOpen = true; 26 | 27 | void DrawerToggle() 28 | { 29 | _drawerOpen = !_drawerOpen; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/ConfirmDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace BlazorGrpc.Components 4 | { 5 | public partial class ConfirmDialog 6 | { 7 | [Parameter] 8 | public string Title { get; set; } 9 | [Parameter] 10 | public string Message { get; set; } 11 | 12 | [Parameter] 13 | public EventCallback OnConfirmed { get; set; } 14 | 15 | [Parameter] 16 | public EventCallback OnCancel { get; set; } 17 | 18 | 19 | [Parameter] 20 | public bool Show { get; set; } 21 | 22 | private async Task Confirm(bool isConfirmed) 23 | { 24 | await OnConfirmed.InvokeAsync(isConfirmed); 25 | } 26 | 27 | private async Task Cancel(bool isConfirmed) 28 | { 29 | await OnCancel.InvokeAsync(isConfirmed); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Layout/NavMenu.razor: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/App.razor: -------------------------------------------------------------------------------- 1 | @using MudBlazor 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Handler/GetAllProductHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorAppData.Interrface; 2 | using BlazorGrpc.Model; 3 | using BlazorGrpcSimpleMediater; 4 | 5 | namespace BlazorGrpc.Handler; 6 | 7 | public class GetAllProductCommand : IRequest> 8 | { 9 | 10 | } 11 | 12 | public record GetAllProductResponse(int Id, string Name, decimal Price); 13 | 14 | public class GetAllProductHandler : IHandler> 15 | { 16 | private readonly IProductRepository _productRepository; 17 | public GetAllProductHandler(IProductRepository productRepository) => _productRepository = productRepository; 18 | 19 | public async Task> Handle(GetAllProductCommand request) 20 | { 21 | var products = await _productRepository.GetProductsAsync() ?? Enumerable.Empty(); 22 | 23 | var response = new List(); 24 | 25 | response.AddRange(products.Select(x => new GetAllProductResponse(x.Id, x.Name.Value, x.Price.Value))); 26 | 27 | return response; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/productService.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option csharp_namespace = "BlazorGrpc.gRPC"; 4 | 5 | package Product; 6 | 7 | service ProductService { 8 | rpc GetProduct (GetProductRequest) returns (ProductResponse); 9 | rpc CreateProduct (CreateProductRequest) returns (ProductResponse); 10 | rpc UpdateProduct (UpdateProductRequest) returns (ProductResponse); 11 | rpc DeleteProduct (DeleteProductRequest) returns (Empty); 12 | rpc ListProducts (Empty) returns (ProductListResponse); 13 | } 14 | 15 | message GetProductRequest { 16 | int32 id = 1; 17 | } 18 | 19 | message CreateProductRequest { 20 | string name = 1; 21 | float price = 2; 22 | } 23 | 24 | message UpdateProductRequest { 25 | int32 id = 1; 26 | string name = 2; 27 | float price = 3; 28 | } 29 | 30 | message DeleteProductRequest { 31 | int32 id = 1; 32 | } 33 | 34 | message ProductResponse { 35 | int32 id = 1; 36 | string name = 2; 37 | float price = 3; 38 | } 39 | 40 | message ProductListResponse { 41 | repeated ProductResponse products = 1; 42 | } 43 | 44 | message Empty {} 45 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/ConfirmDialog.razor: -------------------------------------------------------------------------------- 1 | @rendermode InteractiveServer 2 | 3 | @if (Show){ 4 | 27 | } 28 | -------------------------------------------------------------------------------- /BlazorAppData/DbContext/ProductContext.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | namespace BlazorGrpc.Model; 6 | 7 | public class ProductContext : DbContext 8 | { 9 | public ProductContext(DbContextOptions options) 10 | : base(options) 11 | { 12 | } 13 | 14 | public DbSet Products { get; set; } 15 | 16 | protected override void OnModelCreating(ModelBuilder modelBuilder) 17 | { 18 | modelBuilder.Entity(entity => 19 | { 20 | 21 | entity.OwnsOne(p => p.Price, price => 22 | { 23 | price.Property(p => p.Value) 24 | .HasColumnName("Price") 25 | .IsRequired(); 26 | }); 27 | entity.Navigation(x => x.Price).IsRequired(); 28 | 29 | 30 | entity.OwnsOne(p => p.Name, name => 31 | { 32 | name.Property(n => n.Value) 33 | .HasColumnName("Name") 34 | .IsRequired(); 35 | }); 36 | 37 | entity.Navigation(x => x.Name).IsRequired(); 38 | 39 | }); 40 | } 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Spyros Ponaris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Handler/GetByIdProductHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorAppData.Interrface; 2 | using BlazorGrpcSimpleMediater; 3 | using Grpc.Core; 4 | 5 | namespace BlazorGrpc.Handler; 6 | 7 | public class GetProductByIdCommand : IRequest 8 | { 9 | public int Id { get; } 10 | 11 | public GetProductByIdCommand(int id) => this.Id = id; 12 | } 13 | 14 | public record GetByIdProductResponse(int Id, string Name, decimal Price); 15 | 16 | public class GetByIdProductHandler : IHandler 17 | { 18 | private readonly IProductRepository _productRepository; 19 | public GetByIdProductHandler(IProductRepository productRepository) => _productRepository = productRepository; 20 | public async Task Handle(GetProductByIdCommand request) 21 | { 22 | var product = await _productRepository.GetProductByIdAsync(request.Id); 23 | 24 | if (product == null) 25 | { 26 | throw new RpcException(new Status(StatusCode.NotFound, "Product not found")); 27 | } 28 | 29 | return new GetByIdProductResponse(product.Id, product.Name.Value, product.Price.Value); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Handler/DeleteByIdProductHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorAppData.Interrface; 2 | using BlazorGrpcSimpleMediater; 3 | using Grpc.Core; 4 | 5 | namespace BlazorGrpc.Handler; 6 | 7 | public class DeleteProductByIdCommand : IRequest 8 | { 9 | public int Id { get; } 10 | 11 | public DeleteProductByIdCommand(int id) => this.Id = id; 12 | } 13 | 14 | 15 | public class DeleteByIdProductHandler : IHandler 16 | { 17 | private readonly IProductRepository _productRepository; 18 | private readonly IUnitOfWork _unitOfWork; 19 | public DeleteByIdProductHandler(IProductRepository productRepository, IUnitOfWork unitOfWork) 20 | { 21 | _productRepository = productRepository; 22 | _unitOfWork = unitOfWork; 23 | } 24 | public async Task Handle(DeleteProductByIdCommand request) 25 | { 26 | 27 | await _productRepository.DeleteProductAsync(request.Id); 28 | 29 | var isDeleted = await _unitOfWork.Commit(CancellationToken.None) > 0; 30 | 31 | if (!isDeleted) 32 | throw new RpcException(new Status(StatusCode.NotFound, "Product Could not be Deleted !!!!")); 33 | 34 | return isDeleted; 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /BlazorAppDemo/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:23121", 8 | "sslPort": 44373 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5077", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7294;http://localhost:5077", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /BlazorGrpc.FluentValidation/Validator.cs: -------------------------------------------------------------------------------- 1 | namespace BlazorGrpc.FluentValidation; 2 | 3 | public abstract class Validator : IValidator where T : class 4 | { 5 | private readonly List> _rules = []; 6 | 7 | public virtual void AddRule(Func rule) 8 | { 9 | _rules.Add(rule); 10 | } 11 | 12 | public virtual void AddRule(Func rule, Action action) 13 | { 14 | _rules.Add(rule); 15 | 16 | action(); 17 | } 18 | 19 | public ValidationResult Validate(T instance) 20 | { 21 | var validationResult = new ValidationResult(); 22 | 23 | foreach (var rule in _rules) 24 | { 25 | var result = rule(instance); 26 | if (!result.IsValid) 27 | { 28 | validationResult.AddErrors(result.Errors); 29 | } 30 | } 31 | 32 | return validationResult; 33 | } 34 | 35 | public PropertyRule RuleFor(Func propertyFunc, string propertyName) 36 | where TProperty : IComparable 37 | { 38 | var rule = new PropertyRule(propertyFunc, propertyName, this); 39 | 40 | return rule; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BlazorAppData/Migrations/20240729093035_InitialCreate.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace BlazorGrpc.Migrations 6 | { 7 | /// 8 | public partial class InitialCreate : Migration 9 | { 10 | /// 11 | protected override void Up(MigrationBuilder migrationBuilder) 12 | { 13 | migrationBuilder.CreateTable( 14 | name: "Products", 15 | columns: table => new 16 | { 17 | Id = table.Column(type: "INTEGER", nullable: false) 18 | .Annotation("Sqlite:Autoincrement", true), 19 | Name = table.Column(type: "TEXT", nullable: false), 20 | Price = table.Column(type: "TEXT", nullable: false) 21 | }, 22 | constraints: table => 23 | { 24 | table.PrimaryKey("PK_Products", x => x.Id); 25 | }); 26 | } 27 | 28 | /// 29 | protected override void Down(MigrationBuilder migrationBuilder) 30 | { 31 | migrationBuilder.DropTable( 32 | name: "Products"); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /BlazorAppData/Migrations/ProductContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using BlazorGrpc.Model; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | 7 | #nullable disable 8 | 9 | namespace BlazorGrpc.Migrations 10 | { 11 | [DbContext(typeof(ProductContext))] 12 | partial class ProductContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); 18 | 19 | modelBuilder.Entity("BlazorGrpc.Model.Product", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd() 23 | .HasColumnType("INTEGER"); 24 | 25 | b.Property("Name") 26 | .IsRequired() 27 | .HasColumnType("TEXT"); 28 | 29 | b.Property("Price") 30 | .HasColumnType("TEXT"); 31 | 32 | b.HasKey("Id"); 33 | 34 | b.ToTable("Products"); 35 | }); 36 | #pragma warning restore 612, 618 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/Error.razor: -------------------------------------------------------------------------------- 1 | @page "/Error" 2 | @using System.Diagnostics 3 | 4 | Error 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (ShowRequestId) 10 | { 11 |

12 | Request ID: @RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | The Development environment shouldn't be enabled for deployed applications. 22 | It can result in displaying sensitive information from exceptions to end users. 23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 24 | and restarting the app. 25 |

26 | 27 | @code{ 28 | [CascadingParameter] 29 | private HttpContext? HttpContext { get; set; } 30 | 31 | private string? RequestId { get; set; } 32 | private bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 33 | 34 | protected override void OnInitialized() => 35 | RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier; 36 | } 37 | -------------------------------------------------------------------------------- /BlazorGrpcSimpleMediater/Mediator.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace BlazorGrpcSimpleMediater; 5 | 6 | public class Mediator : IMediator 7 | { 8 | private readonly IServiceProvider _serviceProvider; 9 | 10 | public Mediator(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider; 11 | 12 | public async Task Send(IRequest request) 13 | { 14 | var handlerType = typeof(IHandler<,>).MakeGenericType(request.GetType(), typeof(TResponse)); 15 | dynamic? handler = _serviceProvider.GetService(handlerType); 16 | 17 | if (handler is not null) 18 | { 19 | return await handler.Handle((dynamic)request); 20 | } 21 | 22 | throw new InvalidOperationException($"Not Valid Handler, type : {request.GetType()}"); 23 | 24 | } 25 | 26 | public Task PublishNotification(TEvent @event) where TEvent : INotification 27 | { 28 | var publishers = _serviceProvider.GetServices>(); 29 | 30 | foreach (var publisher in publishers) 31 | { 32 | Task.Factory.StartNew(async (x) => { 33 | await publisher.Publish(@event); 34 | },TaskCreationOptions.LongRunning); 35 | 36 | } 37 | 38 | return Task.CompletedTask; 39 | } 40 | } -------------------------------------------------------------------------------- /BlazorAppData/Migrations/20240729093035_InitialCreate.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using BlazorGrpc.Model; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Infrastructure; 5 | using Microsoft.EntityFrameworkCore.Migrations; 6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 7 | 8 | #nullable disable 9 | 10 | namespace BlazorGrpc.Migrations 11 | { 12 | [DbContext(typeof(ProductContext))] 13 | [Migration("20240729093035_InitialCreate")] 14 | partial class InitialCreate 15 | { 16 | /// 17 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder.HasAnnotation("ProductVersion", "8.0.7"); 21 | 22 | modelBuilder.Entity("BlazorGrpc.Model.Product", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("INTEGER"); 27 | 28 | b.Property("Name") 29 | .IsRequired() 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("Price") 33 | .HasColumnType("TEXT"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("Products"); 38 | }); 39 | #pragma warning restore 612, 618 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/ProductAdd.razor.cs: -------------------------------------------------------------------------------- 1 | 2 | using BlazorGrpc.Model; 3 | using BlazorGrpc.Service; 4 | using BlazorGrpc.Shared; 5 | 6 | using Microsoft.AspNetCore.Components; 7 | 8 | using MudBlazor; 9 | 10 | namespace BlazorGrpc.Components.Pages; 11 | 12 | public partial class ProductAdd 13 | { 14 | [Inject] 15 | protected ServerProductService? ServerProduct { get; set; } 16 | 17 | [SupplyParameterFromForm] 18 | public ProductDto product { get; set; } 19 | protected override Task OnInitializedAsync() 20 | { 21 | product = new ProductDto 22 | { 23 | Id = 0, 24 | Name = string.Empty, 25 | Price = 0m 26 | }; 27 | 28 | return base.OnInitializedAsync(); 29 | } 30 | 31 | private async Task OnSubmit() 32 | { 33 | 34 | try 35 | { 36 | var response = await ServerProduct.CreateProduct(new gRPC.CreateProductRequest 37 | { 38 | Name = product.Name, 39 | Price = (float)product.Price 40 | }, null); 41 | 42 | 43 | Snackbar.Add("Product Added Successfully", Severity.Info); 44 | 45 | product = new ProductDto 46 | { 47 | Id = 0, 48 | Name = string.Empty, 49 | Price = 0m 50 | }; 51 | 52 | } 53 | catch (Exception ex) 54 | { 55 | Snackbar.Add(ex.Message, Severity.Error); 56 | } 57 | 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/ProductAdd.razor: -------------------------------------------------------------------------------- 1 | @page "/productadd" 2 | @using MudBlazor 3 | @rendermode InteractiveAuto 4 | @inject ISnackbar Snackbar 5 | 6 |

Add Product

7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 | 28 | 38 | 39 |
40 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:39326", 8 | "sslPort": 44347 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 17 | "applicationUrl": "http://localhost:5188", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 27 | "applicationUrl": "https://localhost:7048;http://localhost:5188", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/ProductEdit.razor: -------------------------------------------------------------------------------- 1 | @page "/producteedit/{ProductId:int}" 2 | @using MudBlazor 3 | @rendermode InteractiveServer 4 | @inject ISnackbar Snackbar 5 | 6 | 7 | 8 |
9 | 10 |

Details for @product.Id @product.Name

11 | 12 | 14 | 15 | 16 | 17 |
18 | 19 |
20 | 21 | 22 |
23 |
24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 | 35 | 36 | Go to Products 37 | 38 |
39 | 40 |
41 | 42 | -------------------------------------------------------------------------------- /BlazorAppData/BlazorAppData.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/ProductEdit.razor.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpc.Service; 2 | using BlazorGrpc.Shared; 3 | 4 | using Microsoft.AspNetCore.Components; 5 | 6 | using MudBlazor; 7 | 8 | namespace BlazorGrpc.Components.Pages; 9 | 10 | public partial class ProductEdit 11 | { 12 | [Inject] 13 | protected ServerProductService? ServerProduct { get; set; } 14 | 15 | [Parameter] 16 | public int ProductId { get; set; } 17 | 18 | [SupplyParameterFromForm] 19 | public ProductDto product { get; set; } = new ProductDto 20 | { 21 | Id = 0, 22 | Name = string.Empty, 23 | Price = 0m 24 | }; 25 | protected string StatusClass = string.Empty; 26 | protected override async Task OnInitializedAsync() 27 | { 28 | var response = await ServerProduct.GetProduct(new gRPC.GetProductRequest { Id = ProductId }, null); 29 | 30 | if (response is not null ) 31 | { 32 | product.Price = (decimal)response.Price; 33 | product.Id = response.Id; 34 | product.Name = response.Name; 35 | } 36 | 37 | } 38 | 39 | private async Task OnSubmit() 40 | { 41 | try 42 | { 43 | var response = await ServerProduct.UpdateProduct(new gRPC.UpdateProductRequest 44 | { 45 | Name = product.Name, 46 | Price = (float)product.Price, 47 | Id = product.Id 48 | }, null); 49 | 50 | 51 | Snackbar.Add("Product Updated Successfully", Severity.Info); 52 | 53 | } 54 | catch (Exception ex) 55 | { 56 | Snackbar.Add(ex.Message, Severity.Error); 57 | } 58 | 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/UIServices/DialogUIService.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpc.Dialogs; 2 | using BlazorGrpc.gRPC; 3 | using BlazorGrpc.Service; 4 | using MudBlazor; 5 | 6 | namespace BlazorGrpc.UIServices 7 | { 8 | public class DialogUIService 9 | { 10 | private readonly IDialogService _dialogService; 11 | private readonly ISnackbar _snackbar; 12 | protected readonly ServerProductService _serverProduct; 13 | public DialogUIService(IDialogService dialogService, ISnackbar snackbar, ServerProductService ServerProduct) 14 | { 15 | _dialogService = dialogService; 16 | _snackbar = snackbar; 17 | _serverProduct = ServerProduct; 18 | } 19 | 20 | public async Task ShowDeleteConfirmationDialog(DeleteProductRequest dto, 21 | string title, string contentText, 22 | Func onConfirm, Func? onCancel = null) 23 | { 24 | var parameters = new DialogParameters 25 | { 26 | { nameof(DeleteConfirmation.ContentText), contentText }, 27 | { nameof(DeleteConfirmation.Command), dto } 28 | }; 29 | 30 | var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true }; 31 | var dialog = _dialogService.Show(title, parameters, options); 32 | 33 | var result = await dialog.Result; 34 | 35 | if (result is not null && !result.Canceled) 36 | { 37 | await _serverProduct.DeleteProduct(dto,null); 38 | 39 | _snackbar.Add($"{"DeleteSuccess"}", Severity.Info); 40 | 41 | await onConfirm(); 42 | } 43 | else if (onCancel != null) 44 | { 45 | await onCancel(); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /BlazorAppData/UnitOfWork/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using BlazorAppData.Interrface; 4 | 5 | using BlazorGrpc.Model; 6 | 7 | using BlazorGrpcSimpleMediater; 8 | 9 | using LazyCache; 10 | 11 | namespace BlazorAppData.UnitOfWork; 12 | 13 | public class UnitOfWork : IUnitOfWork 14 | { 15 | private readonly ProductContext _productContext; 16 | 17 | private readonly IMediator _mediator; 18 | 19 | private readonly IAppCache _cache; 20 | 21 | private bool disposed; 22 | 23 | public UnitOfWork(ProductContext productContext , 24 | IMediator mediator, 25 | IAppCache cache) 26 | { 27 | _productContext = productContext; 28 | 29 | _mediator = mediator; 30 | 31 | _cache = cache; 32 | } 33 | public async Task Commit(CancellationToken cancellationToken) 34 | { 35 | return await _productContext.SaveChangesAsync(cancellationToken); 36 | } 37 | 38 | public async Task CommitAndRemoveCache(CancellationToken cancellationToken, params string[] cacheKeys) 39 | { 40 | var result = await _productContext.SaveChangesAsync(cancellationToken); 41 | 42 | for (int i = 0; i < cacheKeys.Length; i++) 43 | { 44 | string? cacheKey = cacheKeys[i]; 45 | 46 | _cache.Remove(cacheKey); 47 | } 48 | 49 | return result; 50 | } 51 | 52 | public void Dispose() 53 | { 54 | Dispose(true); 55 | GC.SuppressFinalize(this); 56 | } 57 | 58 | protected virtual void Dispose(bool disposing) 59 | { 60 | if (!disposed) 61 | { 62 | if (disposing) 63 | { 64 | _productContext.Dispose(); 65 | } 66 | } 67 | 68 | disposed = true; 69 | } 70 | public Task Rollback() 71 | { 72 | _productContext.ChangeTracker.Entries().ToList().ForEach(x => x.Reload()); 73 | 74 | return Task.CompletedTask; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @attribute [StreamRendering] 3 | 4 | Weather 5 | 6 |

Weather

7 | 8 |

This component demonstrates showing data.

9 | 10 | @if (forecasts == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 | 39 | @code { 40 | private WeatherForecast[]? forecasts; 41 | 42 | protected override async Task OnInitializedAsync() 43 | { 44 | // Simulate asynchronous loading to demonstrate streaming rendering 45 | await Task.Delay(500); 46 | 47 | var startDate = DateOnly.FromDateTime(DateTime.Now); 48 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 49 | forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 50 | { 51 | Date = startDate.AddDays(index), 52 | TemperatureC = Random.Shared.Next(-20, 55), 53 | Summary = summaries[Random.Shared.Next(summaries.Length)] 54 | }).ToArray(); 55 | } 56 | 57 | private class WeatherForecast 58 | { 59 | public DateOnly Date { get; set; } 60 | public int TemperatureC { get; set; } 61 | public string? Summary { get; set; } 62 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/Weather.razor: -------------------------------------------------------------------------------- 1 | @page "/weather" 2 | @attribute [StreamRendering] 3 | 4 | Weather 5 | 6 |

Weather

7 | 8 |

This component demonstrates showing data.

9 | 10 | @if (forecasts == null) 11 | { 12 |

Loading...

13 | } 14 | else 15 | { 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | @foreach (var forecast in forecasts) 27 | { 28 | 29 | 30 | 31 | 32 | 33 | 34 | } 35 | 36 |
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
37 | } 38 | 39 | @code { 40 | private WeatherForecast[]? forecasts; 41 | 42 | protected override async Task OnInitializedAsync() 43 | { 44 | // Simulate asynchronous loading to demonstrate streaming rendering 45 | await Task.Delay(500); 46 | 47 | var startDate = DateOnly.FromDateTime(DateTime.Now); 48 | var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; 49 | forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast 50 | { 51 | Date = startDate.AddDays(index), 52 | TemperatureC = Random.Shared.Next(-20, 55), 53 | Summary = summaries[Random.Shared.Next(summaries.Length)] 54 | }).ToArray(); 55 | } 56 | 57 | private class WeatherForecast 58 | { 59 | public DateOnly Date { get; set; } 60 | public int TemperatureC { get; set; } 61 | public string? Summary { get; set; } 62 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Handler/UpdateProductHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorAppData.Interrface; 2 | using BlazorGrpc.Model; 3 | using BlazorGrpc.Validation; 4 | 5 | using BlazorGrpcSimpleMediater; 6 | using Grpc.Core; 7 | 8 | namespace BlazorGrpc.Handler; 9 | 10 | public class UpdateProductCommand : IRequest 11 | { 12 | public int Id { get; } 13 | public string Name { get; } 14 | public decimal Price { get; } 15 | 16 | public UpdateProductCommand(int id ,string name, decimal price) 17 | { 18 | if (string.IsNullOrWhiteSpace(name)) 19 | { 20 | throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); 21 | } 22 | 23 | if (id <= 0) 24 | throw new ArgumentException($"'{nameof(id)}' cannot be null or empty.", nameof(id)); 25 | 26 | Id = id; 27 | 28 | Name = name; 29 | 30 | Price = price; 31 | } 32 | } 33 | 34 | public record UpdateProductResponse(int Id, string Name, decimal Price); 35 | 36 | public class UpdateProductHandler : IHandler 37 | { 38 | 39 | private readonly IProductRepository _productRepository; 40 | 41 | public UpdateProductHandler(IProductRepository productRepository) 42 | { 43 | _productRepository = productRepository; 44 | } 45 | 46 | 47 | public async Task Handle(UpdateProductCommand request) 48 | { 49 | var product = Product.ProductFactory.Update(request.Id,request.Name, (decimal)request.Price); 50 | 51 | var productValidator = new ProductValidator(); 52 | 53 | var validationResult = productValidator.Validate(product); 54 | 55 | if (validationResult.Errors.Any()) 56 | { 57 | throw new RpcException(new Status(StatusCode.NotFound, string.Join(",", validationResult.Errors))); 58 | } 59 | 60 | await _productRepository.UpdateProductAsync(product); 61 | 62 | return new UpdateProductResponse(product.Id, product.Name.Value, product.Price.Value); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/Home.razor: -------------------------------------------------------------------------------- 1 | @page "/" 2 | @using Microsoft.AspNetCore.Components.QuickGrid 3 | @using BlazorGrpc 4 | @using Microsoft.JSInterop 5 | 6 | @inject IJSRuntime JSRuntime 7 | @rendermode InteractiveServer 8 | 9 | @inject ISnackbar Snackbar 10 | 11 | Home 12 | 13 |
14 | Items per page: 15 | 21 |
22 | 23 | 24 | 25 | Add Product 26 | 27 | 28 | 29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
42 | 43 | 44 |
45 | Page: 46 | @if (pagination.TotalItemCount.HasValue) 47 | { 48 | for (var pageIndex = 0; pageIndex <= pagination.LastPageIndex; pageIndex++) 49 | { 50 | var capturedIndex = pageIndex; 51 | 57 | } 58 | } 59 |
60 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Handler/CreateOrderHandler.cs: -------------------------------------------------------------------------------- 1 | using BlazorAppData.Interrface; 2 | 3 | using BlazorGrpc.Model; 4 | using BlazorGrpc.Validation; 5 | 6 | using BlazorGrpcSimpleMediater; 7 | using Grpc.Core; 8 | namespace BlazorGrpc.Handler; 9 | 10 | public class CreateProductCommand : IRequest 11 | { 12 | public string Name { get; } 13 | public decimal Price { get; } 14 | 15 | public CreateProductCommand(string name, decimal price) 16 | { 17 | if (string.IsNullOrWhiteSpace(name)) 18 | { 19 | throw new ArgumentException($"'{nameof(name)}' cannot be null or empty.", nameof(name)); 20 | } 21 | 22 | Name = name; 23 | 24 | Price = price; 25 | } 26 | } 27 | 28 | public record CreateProductResponse(int Id, string Name, decimal Price); 29 | 30 | public class CreateProductHandler : IHandler 31 | { 32 | private readonly IProductRepository _productRepository; 33 | 34 | private readonly IUnitOfWork _unitOfWork; 35 | 36 | public CreateProductHandler(IProductRepository productRepository, IUnitOfWork unitOfWork) 37 | { 38 | _productRepository = productRepository; 39 | 40 | _unitOfWork = unitOfWork; 41 | 42 | } 43 | 44 | public async Task Handle(CreateProductCommand request) 45 | { 46 | var product = Product.ProductFactory.CreateProduct(request.Name, (decimal)request.Price); 47 | 48 | var productValidator = new ProductValidator(); 49 | 50 | var validationResult = productValidator.Validate(product); 51 | 52 | if (validationResult.Errors.Any()) { 53 | throw new RpcException(new Status(StatusCode.NotFound, string.Join(",", validationResult.Errors))); 54 | } 55 | 56 | await _productRepository.CreateProdutAsync(product); 57 | 58 | var isCreated = await _unitOfWork.Commit(CancellationToken.None) > 0; 59 | 60 | if (!isCreated) 61 | throw new RpcException(new Status(StatusCode.NotFound, "Product Could not be Created !!!!")); 62 | 63 | return new CreateProductResponse(product.Id, product.Name.Value, product.Price.Value); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Layout/MainLayout.razor.css: -------------------------------------------------------------------------------- 1 | .page { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | } 6 | 7 | main { 8 | flex: 1; 9 | } 10 | 11 | .sidebar { 12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); 13 | } 14 | 15 | .top-row { 16 | background-color: #f7f7f7; 17 | border-bottom: 1px solid #d6d5d5; 18 | justify-content: flex-end; 19 | height: 3.5rem; 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .top-row ::deep a, .top-row ::deep .btn-link { 25 | white-space: nowrap; 26 | margin-left: 1.5rem; 27 | text-decoration: none; 28 | } 29 | 30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | .top-row ::deep a:first-child { 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | @media (max-width: 640.98px) { 40 | .top-row { 41 | justify-content: space-between; 42 | } 43 | 44 | .top-row ::deep a, .top-row ::deep .btn-link { 45 | margin-left: 0; 46 | } 47 | } 48 | 49 | @media (min-width: 641px) { 50 | .page { 51 | flex-direction: row; 52 | } 53 | 54 | .sidebar { 55 | width: 250px; 56 | height: 100vh; 57 | position: sticky; 58 | top: 0; 59 | } 60 | 61 | .top-row { 62 | position: sticky; 63 | top: 0; 64 | z-index: 1; 65 | } 66 | 67 | .top-row.auth ::deep a:first-child { 68 | flex: 1; 69 | text-align: right; 70 | width: 0; 71 | } 72 | 73 | .top-row, article { 74 | padding-left: 2rem !important; 75 | padding-right: 1.5rem !important; 76 | } 77 | } 78 | 79 | #blazor-error-ui { 80 | background: lightyellow; 81 | bottom: 0; 82 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); 83 | display: none; 84 | left: 0; 85 | padding: 0.6rem 1.25rem 0.7rem 1.25rem; 86 | position: fixed; 87 | width: 100%; 88 | z-index: 1000; 89 | } 90 | 91 | #blazor-error-ui .dismiss { 92 | cursor: pointer; 93 | position: absolute; 94 | right: 0.75rem; 95 | top: 0.5rem; 96 | } 97 | -------------------------------------------------------------------------------- /BlazorAppData/Model/Product.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace BlazorGrpc.Model; 5 | 6 | 7 | public class Product 8 | { 9 | public int Id { get; set; } 10 | 11 | [Required] 12 | public virtual ProductName Name { get; set; } 13 | 14 | [Required] 15 | public virtual Price Price { get; set; } 16 | 17 | [JsonConstructor] 18 | private Product(){} 19 | 20 | public static class ProductFactory 21 | { 22 | public static Product CreateProduct() 23 | { 24 | return new Product(0, string.Empty,0m); 25 | } 26 | 27 | public static Product CreateProduct(string productName, decimal price) 28 | { 29 | // Add any validation or creation logic here 30 | if (string.IsNullOrWhiteSpace(productName)) 31 | { 32 | throw new ArgumentException("Product name cannot be empty", nameof(productName)); 33 | } 34 | 35 | if (price < 0) 36 | { 37 | throw new ArgumentException("Price cannot be negative", nameof(price)); 38 | } 39 | 40 | return new Product(0,productName, price); 41 | } 42 | 43 | public static Product Update(int id , string productName, decimal price) 44 | { 45 | // Add any validation or creation logic here 46 | if (string.IsNullOrWhiteSpace(productName)) 47 | { 48 | throw new ArgumentException("Product name cannot be empty", nameof(productName)); 49 | } 50 | 51 | if (price < 0) 52 | { 53 | throw new ArgumentException("Price cannot be negative", nameof(price)); 54 | } 55 | 56 | return new Product(id, productName, price); 57 | } 58 | } 59 | 60 | internal Product(int id , string productName, decimal price) 61 | { 62 | Id = id; 63 | 64 | Name = new ProductName { Value = productName }; 65 | 66 | Price = new Price { Value = price }; 67 | } 68 | } 69 | 70 | public record ProductName 71 | { 72 | public string Value { get; set; } 73 | } 74 | 75 | 76 | public record Price 77 | { 78 | public decimal Value { get; set; } 79 | } -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/BlazorGrpc.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | 36 | all 37 | runtime; build; native; contentfiles; analyzers; buildtransitive 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /BlazorAppData/Repository/ProductRepository.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using BlazorAppData.Interrface; 4 | 5 | using BlazorGrpc.Model; 6 | 7 | using Microsoft.EntityFrameworkCore; 8 | 9 | namespace BlazorAppData.Repository; 10 | 11 | public class ProductRepository : IProductRepository 12 | { 13 | private readonly ProductContext _context; 14 | 15 | public ProductRepository(ProductContext context) 16 | { 17 | _context = context; 18 | } 19 | public async Task CreateProdutAsync(Product product) 20 | { 21 | ArgumentNullException.ThrowIfNull(product); 22 | 23 | await _context.Products.AddAsync(product); 24 | } 25 | 26 | public async Task DeleteProductAsync(Product product) 27 | { 28 | ArgumentNullException.ThrowIfNull(product); 29 | 30 | var id = product.Id; 31 | 32 | var productForDelete = await _context.Products.FindAsync(id) ?? throw new ArgumentException($"product with Id : {product.Id} not found !!!"); 33 | 34 | _context.Products.Remove(productForDelete); 35 | 36 | } 37 | 38 | public async Task DeleteProductAsync(int productId) 39 | { 40 | var productForDelete = await _context.Products.FindAsync(productId) ?? throw new ArgumentException($"product with Id : {productId} not found !!!"); 41 | 42 | _context.Products.Remove(productForDelete); 43 | } 44 | 45 | public async Task GetProductByIdAsync(object id) 46 | { 47 | var product = await _context.Products.FindAsync(id); 48 | 49 | return product ?? Product.ProductFactory.CreateProduct(); 50 | 51 | } 52 | 53 | public async Task> GetProductsAsync() 54 | { 55 | return await this._context 56 | .Products 57 | .AsNoTracking() 58 | .ToListAsync(); 59 | } 60 | 61 | public async Task UpdateProductAsync(Product product) 62 | { 63 | ArgumentNullException.ThrowIfNull(product); 64 | 65 | var id = product.Id; 66 | 67 | await _context.Products.Where(x => x.Id == id) 68 | .ExecuteUpdateAsync(setters => setters.SetProperty(x => x.Name.Value ,product.Name.Value) 69 | .SetProperty(x => x.Price.Value, product.Price.Value)); 70 | 71 | 72 | } 73 | } 74 | 75 | 76 | //var productForUpdate = await _context.Products.FindAsync(id) ?? throw new ArgumentException($"product with Id : {product.Id} not found !!!"); 77 | 78 | //productForUpdate.Name = product.Name; 79 | 80 | //productForUpdate.Price = product.Price; 81 | 82 | //_context.Products.Update(productForUpdate); 83 | -------------------------------------------------------------------------------- /BlazorAppDemo/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Service/ServerProductService.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpc.gRPC; 2 | using BlazorGrpc.Handler; 3 | using BlazorGrpc.Notification; 4 | 5 | using BlazorGrpcSimpleMediater; 6 | using Grpc.Core; 7 | 8 | namespace BlazorGrpc.Service; 9 | 10 | public class ServerProductService : ProductService.ProductServiceBase 11 | { 12 | private readonly IMediator _mediator; 13 | public ServerProductService(IMediator mediator) => _mediator = mediator; 14 | 15 | public override async Task GetProduct(GetProductRequest request, ServerCallContext context) 16 | { 17 | var response = await _mediator.Send(new GetProductByIdCommand(request.Id)).ConfigureAwait(false); 18 | 19 | return new ProductResponse 20 | { 21 | Id = response.Id, 22 | Name = response.Name, 23 | Price = (float)response.Price 24 | }; 25 | } 26 | 27 | public override async Task CreateProduct(CreateProductRequest request, ServerCallContext context) 28 | { 29 | var response = await _mediator.Send(new CreateProductCommand(request.Name, (decimal)request.Price)).ConfigureAwait(false); 30 | 31 | var @event = new ProductCreatedNotification($"Product Created {response.Id}"); 32 | 33 | await _mediator.PublishNotification(@event); 34 | 35 | return new ProductResponse 36 | { 37 | Id = response.Id, 38 | Name = response.Name, 39 | Price = (float)response.Price 40 | }; 41 | } 42 | 43 | public override async Task UpdateProduct(UpdateProductRequest request, ServerCallContext context) 44 | { 45 | var response = await _mediator.Send(new UpdateProductCommand(request.Id, request.Name, (decimal)request.Price)).ConfigureAwait(false); 46 | 47 | return new ProductResponse 48 | { 49 | Id = response.Id, 50 | Name = response.Name, 51 | Price = (float)response.Price 52 | }; 53 | } 54 | 55 | public override async Task DeleteProduct(DeleteProductRequest request, ServerCallContext context) 56 | { 57 | var response = await _mediator.Send(new DeleteProductByIdCommand(request.Id)).ConfigureAwait(false); 58 | 59 | if (!response) 60 | throw new RpcException(new Status(StatusCode.NotFound, "Product not found")); 61 | 62 | return new Empty(); 63 | } 64 | 65 | public override async Task ListProducts(Empty request, ServerCallContext context) 66 | { 67 | var response = await _mediator.Send(new GetAllProductCommand()).ConfigureAwait(false); 68 | 69 | var productListResponse = new ProductListResponse(); 70 | 71 | productListResponse.Products.AddRange(response.Select(x => new ProductResponse 72 | { 73 | Id = x.Id, 74 | Name = x.Name, 75 | Price = (float)x.Price 76 | })); 77 | 78 | return productListResponse; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Program.cs: -------------------------------------------------------------------------------- 1 | 2 | using BlazorAppData.Interrface; 3 | using BlazorAppData.Repository; 4 | using BlazorAppData.UnitOfWork; 5 | 6 | using Blazored.LocalStorage; 7 | 8 | using BlazorGrpc.Components; 9 | using BlazorGrpc.Handler; 10 | using BlazorGrpc.Model; 11 | using BlazorGrpc.Notification; 12 | using BlazorGrpc.Service; 13 | using BlazorGrpc.UIServices; 14 | 15 | using BlazorGrpcSimpleMediater; 16 | using Microsoft.EntityFrameworkCore; 17 | 18 | using MudBlazor; 19 | using MudBlazor.Services; 20 | 21 | 22 | var builder = WebApplication.CreateBuilder(args); 23 | 24 | // Add services to the container. 25 | builder.Services.AddRazorComponents() 26 | .AddInteractiveServerComponents() 27 | .AddInteractiveWebAssemblyComponents(); 28 | 29 | builder.Services.AddGrpc(x=>x.EnableDetailedErrors = true); 30 | builder.Services.AddDbContext(options => 31 | options.UseSqlite("Data Source=products.db")); 32 | 33 | builder.Services.AddScoped(); 34 | builder.Services.AddScoped(); 35 | builder.Services.AddScoped(); 36 | 37 | 38 | builder.Services.AddLazyCache(); 39 | builder.Services.AddBlazoredLocalStorage(); 40 | 41 | builder.Services.AddMudServices(config => 42 | { 43 | config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft; 44 | 45 | config.SnackbarConfiguration.PreventDuplicates = false; 46 | config.SnackbarConfiguration.NewestOnTop = false; 47 | config.SnackbarConfiguration.ShowCloseIcon = true; 48 | config.SnackbarConfiguration.VisibleStateDuration = 10000; 49 | config.SnackbarConfiguration.HideTransitionDuration = 500; 50 | config.SnackbarConfiguration.ShowTransitionDuration = 500; 51 | config.SnackbarConfiguration.SnackbarVariant = Variant.Filled; 52 | }); 53 | 54 | builder.Services.AddScoped(); 55 | 56 | builder.Services.AddScoped(typeof(IUnitOfWork<>), typeof(UnitOfWork<>)); 57 | 58 | 59 | builder.Services.AddScoped, CreateProductHandler>(); 60 | builder.Services.AddScoped, GetByIdProductHandler>(); 61 | builder.Services.AddScoped, DeleteByIdProductHandler>(); 62 | builder.Services.AddScoped, UpdateProductHandler>(); 63 | builder.Services.AddScoped>, GetAllProductHandler>(); 64 | builder.Services.AddScoped, ProducCreatedNotificationHandler>(); 65 | 66 | var app = builder.Build(); 67 | 68 | // Configure the HTTP request pipeline. 69 | if (app.Environment.IsDevelopment()) 70 | { 71 | app.UseWebAssemblyDebugging(); 72 | } 73 | else 74 | { 75 | app.UseExceptionHandler("/Error", createScopeForErrors: true); 76 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 77 | app.UseHsts(); 78 | } 79 | 80 | 81 | 82 | app.UseHttpsRedirection(); 83 | 84 | app.UseStaticFiles(); 85 | app.UseAntiforgery(); 86 | 87 | app.MapRazorComponents() 88 | .AddInteractiveServerRenderMode() 89 | .AddInteractiveWebAssemblyRenderMode() 90 | .AddAdditionalAssemblies(typeof(BlazorGrpc.Client._Imports).Assembly); 91 | 92 | app.UseGrpcWeb(new GrpcWebOptions { DefaultEnabled = true }); 93 | app.MapGrpcService() 94 | .EnableGrpcWeb(); 95 | 96 | app.Run(); 97 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Pages/Home.razor.cs: -------------------------------------------------------------------------------- 1 | using BlazorGrpc.Model; 2 | using BlazorGrpc.Service; 3 | using BlazorGrpc.Shared; 4 | using BlazorGrpc.UIServices; 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.AspNetCore.Components.QuickGrid; 7 | using MudBlazor; 8 | namespace BlazorGrpc.Components.Pages; 9 | 10 | public partial class Home 11 | { 12 | [Inject] 13 | protected ServerProductService? ServerProduct { get; set; } 14 | 15 | [Inject] 16 | protected DialogUIService? Dialog { get; set; } 17 | 18 | [Inject] 19 | public NavigationManager NavigationManager { get; set; } 20 | protected List productListResponse { get; set; } = []; 21 | private IQueryable? ProductIQueryable { get; set; } 22 | 23 | protected PaginationState pagination = new() { ItemsPerPage = 5 }; 24 | 25 | protected bool loading = false; 26 | 27 | private QuickGrid grid; 28 | 29 | private bool ShowMessageBox { get; set; } = false; 30 | 31 | private ProductDto CurrentProduct { get; set; } = default!; 32 | protected override async Task OnInitializedAsync() 33 | { 34 | try 35 | { 36 | await FillData(); 37 | } 38 | catch (Exception ex) 39 | { 40 | Snackbar.Add(ex.Message, Severity.Error); 41 | } 42 | 43 | } 44 | 45 | private async ValueTask FillData() 46 | { 47 | loading = true; 48 | 49 | (productListResponse ??= new List()).Clear(); 50 | 51 | var response = await ServerProduct.ListProducts(null, null); 52 | 53 | if (response != null) 54 | { 55 | 56 | foreach (var item in response.Products) 57 | { 58 | productListResponse.Add(new ProductDto 59 | { 60 | Id = item.Id, 61 | Name = item.Name, 62 | Price = (decimal)item.Price 63 | }); 64 | } 65 | 66 | ProductIQueryable = productListResponse.AsQueryable(); 67 | 68 | pagination.TotalItemCountChanged += (sender, eventArgs) => StateHasChanged(); 69 | } 70 | 71 | StateHasChanged(); 72 | 73 | loading = false; 74 | } 75 | 76 | 77 | private Task EditRow(ProductDto product) 78 | { 79 | NavigationManager.NavigateTo($"producteedit/{product.Id}"); 80 | 81 | return Task.CompletedTask; 82 | } 83 | 84 | private async Task Delete(ProductDto product) 85 | { 86 | var contentText = $"Delete Confirmation {product.Name}"; 87 | 88 | await Dialog.ShowDeleteConfirmationDialog(new gRPC.DeleteProductRequest 89 | { 90 | Id = product.Id 91 | }, "Delete ConfirmationTitle ", contentText, 92 | async () => 93 | { 94 | await FillData(); 95 | } 96 | ); 97 | } 98 | 99 | 100 | private void OnRowEdit(ChangeEventArgs e) 101 | { 102 | // Handle input change events 103 | var input = e.Value.ToString(); 104 | // Update the model based on the input 105 | } 106 | 107 | private async Task ShowConfirmDialog(Product product) 108 | { 109 | ShowMessageBox = true; 110 | } 111 | 112 | private async Task GoToPageAsync(int pageIndex) 113 | { 114 | await pagination.SetCurrentPageIndexAsync(pageIndex); 115 | } 116 | 117 | private string? PageButtonClass(int pageIndex) 118 | => pagination.CurrentPageIndex == pageIndex ? "current" : null; 119 | 120 | private string? AriaCurrentValue(int pageIndex) 121 | => pagination.CurrentPageIndex == pageIndex ? "page" : null; 122 | } 123 | -------------------------------------------------------------------------------- /BlazorGrpc.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.10.35004.147 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorGrpc", "BlazorGrpc\BlazorGrpc\BlazorGrpc.csproj", "{D29A9525-A41A-4ED8-AF46-903B917B59F6}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorGrpc.Client", "BlazorGrpc\BlazorGrpc.Client\BlazorGrpc.Client.csproj", "{C3212226-8CF5-4A64-8B21-4D1E0EDDE8FF}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorAppData", "BlazorAppData\BlazorAppData.csproj", "{D0251030-FB10-4E2C-9582-B828A8DC5295}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorGrpcSimpleMediater", "BlazorGrpcSimpleMediater\BlazorGrpcSimpleMediater.csproj", "{F902C181-19DF-4351-89C1-448C1F0233C0}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorGrpc.FluentValidation", "BlazorGrpc.FluentValidation\BlazorGrpc.FluentValidation.csproj", "{CCF54559-BE9E-4DF4-AE06-283F04956626}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorGrpc.Shared", "BlazorGrpc.Shared\BlazorGrpc.Shared.csproj", "{BAC2833F-EB31-4C82-AD01-1D4143FCD948}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {D29A9525-A41A-4ED8-AF46-903B917B59F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {D29A9525-A41A-4ED8-AF46-903B917B59F6}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {D29A9525-A41A-4ED8-AF46-903B917B59F6}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {D29A9525-A41A-4ED8-AF46-903B917B59F6}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {C3212226-8CF5-4A64-8B21-4D1E0EDDE8FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {C3212226-8CF5-4A64-8B21-4D1E0EDDE8FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {C3212226-8CF5-4A64-8B21-4D1E0EDDE8FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {C3212226-8CF5-4A64-8B21-4D1E0EDDE8FF}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {D0251030-FB10-4E2C-9582-B828A8DC5295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {D0251030-FB10-4E2C-9582-B828A8DC5295}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {D0251030-FB10-4E2C-9582-B828A8DC5295}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {D0251030-FB10-4E2C-9582-B828A8DC5295}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {F902C181-19DF-4351-89C1-448C1F0233C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {F902C181-19DF-4351-89C1-448C1F0233C0}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {F902C181-19DF-4351-89C1-448C1F0233C0}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {F902C181-19DF-4351-89C1-448C1F0233C0}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {CCF54559-BE9E-4DF4-AE06-283F04956626}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {CCF54559-BE9E-4DF4-AE06-283F04956626}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {CCF54559-BE9E-4DF4-AE06-283F04956626}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {CCF54559-BE9E-4DF4-AE06-283F04956626}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {BAC2833F-EB31-4C82-AD01-1D4143FCD948}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {BAC2833F-EB31-4C82-AD01-1D4143FCD948}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {BAC2833F-EB31-4C82-AD01-1D4143FCD948}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {BAC2833F-EB31-4C82-AD01-1D4143FCD948}.Release|Any CPU.Build.0 = Release|Any CPU 48 | EndGlobalSection 49 | GlobalSection(SolutionProperties) = preSolution 50 | HideSolutionNode = FALSE 51 | EndGlobalSection 52 | GlobalSection(ExtensibilityGlobals) = postSolution 53 | SolutionGuid = {A20C8F87-0936-49E8-B308-71D83E620FC4} 54 | EndGlobalSection 55 | EndGlobal 56 | -------------------------------------------------------------------------------- /BlazorAppDemo/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep .nav-link { 63 | color: #d7d7d7; 64 | background: none; 65 | border: none; 66 | border-radius: 4px; 67 | height: 3rem; 68 | display: flex; 69 | align-items: center; 70 | line-height: 3rem; 71 | width: 100%; 72 | } 73 | 74 | .nav-item ::deep a.active { 75 | background-color: rgba(255,255,255,0.37); 76 | color: white; 77 | } 78 | 79 | .nav-item ::deep .nav-link:hover { 80 | background-color: rgba(255,255,255,0.1); 81 | color: white; 82 | } 83 | 84 | .nav-scrollable { 85 | display: none; 86 | } 87 | 88 | .navbar-toggler:checked ~ .nav-scrollable { 89 | display: block; 90 | } 91 | 92 | @media (min-width: 641px) { 93 | .navbar-toggler { 94 | display: none; 95 | } 96 | 97 | .nav-scrollable { 98 | /* Never collapse the sidebar for wide screens */ 99 | display: block; 100 | 101 | /* Allow sidebar to scroll for tall menus */ 102 | height: calc(100vh - 3.5rem); 103 | overflow-y: auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/Components/Layout/NavMenu.razor.css: -------------------------------------------------------------------------------- 1 | .navbar-toggler { 2 | appearance: none; 3 | cursor: pointer; 4 | width: 3.5rem; 5 | height: 2.5rem; 6 | color: white; 7 | position: absolute; 8 | top: 0.5rem; 9 | right: 1rem; 10 | border: 1px solid rgba(255, 255, 255, 0.1); 11 | background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); 12 | } 13 | 14 | .navbar-toggler:checked { 15 | background-color: rgba(255, 255, 255, 0.5); 16 | } 17 | 18 | .top-row { 19 | height: 3.5rem; 20 | background-color: rgba(0,0,0,0.4); 21 | } 22 | 23 | .navbar-brand { 24 | font-size: 1.1rem; 25 | } 26 | 27 | .bi { 28 | display: inline-block; 29 | position: relative; 30 | width: 1.25rem; 31 | height: 1.25rem; 32 | margin-right: 0.75rem; 33 | top: -1px; 34 | background-size: cover; 35 | } 36 | 37 | .bi-house-door-fill-nav-menu { 38 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); 39 | } 40 | 41 | .bi-plus-square-fill-nav-menu { 42 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); 43 | } 44 | 45 | .bi-list-nested-nav-menu { 46 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); 47 | } 48 | 49 | .nav-item { 50 | font-size: 0.9rem; 51 | padding-bottom: 0.5rem; 52 | } 53 | 54 | .nav-item:first-of-type { 55 | padding-top: 1rem; 56 | } 57 | 58 | .nav-item:last-of-type { 59 | padding-bottom: 1rem; 60 | } 61 | 62 | .nav-item ::deep .nav-link { 63 | color: #d7d7d7; 64 | background: none; 65 | border: none; 66 | border-radius: 4px; 67 | height: 3rem; 68 | display: flex; 69 | align-items: center; 70 | line-height: 3rem; 71 | width: 100%; 72 | } 73 | 74 | .nav-item ::deep a.active { 75 | background-color: rgba(255,255,255,0.37); 76 | color: white; 77 | } 78 | 79 | .nav-item ::deep .nav-link:hover { 80 | background-color: rgba(255,255,255,0.1); 81 | color: white; 82 | } 83 | 84 | .nav-scrollable { 85 | display: none; 86 | } 87 | 88 | .navbar-toggler:checked ~ .nav-scrollable { 89 | display: block; 90 | } 91 | 92 | @media (min-width: 641px) { 93 | .navbar-toggler { 94 | display: none; 95 | } 96 | 97 | .nav-scrollable { 98 | /* Never collapse the sidebar for wide screens */ 99 | display: block; 100 | 101 | /* Allow sidebar to scroll for tall menus */ 102 | height: calc(100vh - 3.5rem); 103 | overflow-y: auto; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /BlazorGrpc/BlazorGrpc/wwwroot/app.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | } 4 | 5 | a, .btn-link { 6 | color: #006bb7; 7 | } 8 | 9 | .btn-primary { 10 | color: #fff; 11 | background-color: #1b6ec2; 12 | border-color: #1861ac; 13 | } 14 | 15 | .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus { 16 | box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb; 17 | } 18 | 19 | .content { 20 | padding-top: 1.1rem; 21 | } 22 | 23 | h1:focus { 24 | outline: none; 25 | } 26 | 27 | .valid.modified:not([type=checkbox]) { 28 | outline: 1px solid #26b050; 29 | } 30 | 31 | .invalid { 32 | outline: 1px solid #e50000; 33 | } 34 | 35 | .validation-message { 36 | color: #e50000; 37 | } 38 | 39 | .blazor-error-boundary { 40 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121; 41 | padding: 1rem 1rem 1rem 3.7rem; 42 | color: white; 43 | } 44 | 45 | .blazor-error-boundary::after { 46 | content: "An error has occurred." 47 | } 48 | 49 | .darker-border-checkbox.form-check-input { 50 | border-color: #929292; 51 | } 52 | 53 | ::deep th.country-name { 54 | width: 14rem; 55 | } 56 | 57 | /* Subtle stripe effect */ 58 | ::deep tr:nth-child(even) { 59 | background: rgba(255,255,255,0.4); 60 | } 61 | 62 | /* Don't collapse rows even if they are empty */ 63 | ::deep tbody tr { 64 | height: 1.8rem; 65 | } 66 | 67 | /* Style the custom page links*/ 68 | .page-buttons { 69 | margin: 1rem 0; 70 | align-items: center; 71 | } 72 | 73 | .page-buttons button { 74 | background: #d6d7d8; 75 | color: black; 76 | padding: 0.25rem 0.75rem; 77 | border-radius: 0.4rem; 78 | transition: transform 0.3s ease-out; 79 | margin: 0.25rem; 80 | } 81 | 82 | .page-buttons button:active { 83 | background: #a7c1ff !important; 84 | color: white; 85 | transform: scale(0.95) translateY(-0.15rem); 86 | transition-duration: 0.05s; 87 | } 88 | 89 | .page-buttons button:hover:not(.current) { 90 | background: #c0c9dc; 91 | } 92 | 93 | .page-buttons button.current { 94 | background: #3771f4; 95 | color: white; 96 | } -------------------------------------------------------------------------------- /BlazorGrpc.FluentValidation/PropertyRule.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace BlazorGrpc.FluentValidation 4 | { 5 | public class PropertyRule(Func propertyFunc, string propertyName, Validator validator) 6 | where T : class 7 | where TProperty : IComparable 8 | { 9 | private readonly Func _propertyFunc = propertyFunc; 10 | private readonly string _propertyName = propertyName; 11 | private readonly Validator _validator = validator; 12 | private List>> _expressions = []; 13 | 14 | public PropertyRule NotEmpty() 15 | { 16 | _validator.AddRule(instance => 17 | { 18 | var value = _propertyFunc(instance); 19 | 20 | var isValid = value is not null 21 | && !value.Equals(default(TProperty)); 22 | 23 | var result = new ValidationResult(); 24 | 25 | if (!isValid) 26 | { 27 | result.AddError($"{_propertyName} must not be empty."); 28 | } 29 | 30 | return result; 31 | }); 32 | 33 | return this; 34 | 35 | 36 | 37 | } 38 | 39 | public PropertyRule Length(int min, int max) 40 | { 41 | _validator.AddRule(instance => 42 | { 43 | var value = _propertyFunc(instance)?.ToString(); 44 | var isValid = value is not null && value.Length >= min && value.Length <= max; 45 | 46 | var result = new ValidationResult(); 47 | 48 | if (!isValid) 49 | { 50 | result.AddError($"{_propertyName} length must be between {min} and {max} characters."); 51 | } 52 | 53 | return result; 54 | }); 55 | 56 | return this; 57 | } 58 | 59 | public PropertyRule Length(int min, int max, string errorMessage) 60 | { 61 | _validator.AddRule(instance => 62 | { 63 | var value = _propertyFunc(instance)?.ToString(); 64 | var isValid = value is not null && value.Length >= min && value.Length <= max; 65 | 66 | var result = new ValidationResult(); 67 | 68 | if (!isValid) 69 | { 70 | result.AddError(errorMessage); 71 | } 72 | 73 | return result; 74 | }); 75 | 76 | return this; 77 | } 78 | 79 | public PropertyRule AddRule(Expression> expression, string errorMessage) 80 | { 81 | _validator.AddRule(instance => 82 | { 83 | var compiled = expression.Compile(); 84 | var isValid = compiled.Invoke(instance); 85 | 86 | var result = new ValidationResult(); 87 | 88 | if (!isValid) 89 | { 90 | result.AddError(errorMessage); 91 | } 92 | return result; 93 | }); 94 | 95 | _expressions.Add(expression); 96 | 97 | return this; 98 | } 99 | 100 | public PropertyRule GreaterThan(TProperty threshold) 101 | { 102 | _validator.AddRule(instance => 103 | { 104 | var value = _propertyFunc(instance); 105 | 106 | var result = new ValidationResult(); 107 | 108 | if (!(value != null && value.CompareTo(threshold) > 0)) 109 | { 110 | result.AddError($"{_propertyName} must be greater than {threshold}."); 111 | } 112 | 113 | return result; 114 | }); 115 | 116 | return this; 117 | } 118 | } 119 | } 120 | 121 | 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blazor & Grpc 2 | 3 | BlazorGrpc is a project demonstrating the integration of Blazor and gRPC for building a real-time, highly responsive web application using modern web technologies. This project aims to showcase how Blazor WebAssembly and gRPC can be used together to create efficient and scalable web applications. It also includes a custom implementation of the MediatR pattern to manage application logic and messaging. 4 | 5 | ## Features 6 | 7 | - **Blazor WebAssembly**: Client-side web UI framework using C# and .NET. 8 | - **QuickGrid Integration**: Includes QuickGrid, a powerful and lightweight grid component tailored for Blazor applications, offering high performance, customization, and the ability to efficiently handle large datasets.https://aspnet.github.io/quickgridsamples/ 9 | - **MudBlazor**: Includes MudBlazor Components https://mudblazor.com/ 10 | - **gRPC**: High-performance, open-source RPC framework that can run in any environment. 11 | - **Real-time Communication**: Utilize gRPC to establish real-time communication between the client and server. 12 | - **Scalable Architecture**: Designed to handle high loads and scale efficiently. 13 | - **Custom MediatR Implementation**: A custom implementation of the MediatR pattern to facilitate in-process messaging and command handling , inspired by the MediatR library 14 | https://github.com/jbogard/MediatR 15 | - **Repository Pattern & Unit of Work**: Implements the Repository pattern and Unit of Work to manage data persistence, providing a clean separation of concerns. 16 | - **Entity Framework Core & SQLite**: Utilizes EF Core and SQLite for data storage, enabling a lightweight and efficient database solution. https://learn.microsoft.com/en-us/ef/core/ 17 | - **Simple Fluent Validation Library**: This repository contains a Simple Fluent Validation Library, a lightweight validation framework inspired by the FluentValidation library 18 | 19 | ## Getting Started 20 | 21 | ### Prerequisites 22 | 23 | Ensure you have the following installed: 24 | 25 | - [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) 26 | - [Visual Studio 2022](https://visualstudio.microsoft.com/vs/) (or later) with ASP.NET and Blazor web development workload 27 | 28 | ### Installation 29 | 30 | 1. **Clone the repository**: 31 | 32 | ```sh 33 | git clone https://github.com/stevsharp/BlazorGrpc.git 34 | cd BlazorGrpc 35 | ``` 36 | 37 | 2. **Restore dependencies**: 38 | 39 | ```sh 40 | dotnet restore 41 | ``` 42 | 43 | 3. **Build the project**: 44 | 45 | ```sh 46 | dotnet build 47 | ``` 48 | 49 | ### Running the Application 50 | 51 | 1. **Start the gRPC server**: 52 | 53 | ```sh 54 | dotnet run --project Server/BlazorGrpc.Server.csproj 55 | ``` 56 | 2. Open your browser and navigate to `https://localhost:5001` to see the application in action. 57 | 58 | ## Project Structure 59 | 60 | - **Server**: Contains the gRPC server implementation using ASP.NET Core. 61 | - **Client**: Blazor WebAssembly project acting as the frontend. 62 | - **Shared**: Shared project containing common code and proto files used by both the server and client. 63 | - **CustomMediatR**: A custom implementation of the MediatR pattern for managing in-process messaging and command handling. 64 | 65 | ## Usage 66 | 67 | The BlazorGrpc project demonstrates basic CRUD operations using gRPC and includes a custom MediatR implementation to manage application logic. It serves as a starting point for building more complex applications using Blazor and gRPC. 68 | 69 | ### Custom MediatR Implementation 70 | 71 | The custom implementation provides an example of how to use in-process messaging to decouple components within an application. To use it: 72 | 73 | 1. **Define Request and Response Types**: Create request and response classes for your commands and queries. 74 | 2. **Implement Handlers**: Create handler classes that implement the logic for processing each request. 75 | 3. **Register Handlers**: Use dependency injection to register your handlers in the service container. 76 | 4. **Send Requests**: Use the mediator to send requests and handle responses. 77 | 78 | ### Adding a New Feature 79 | 80 | 1. Define your gRPC service in a `.proto` file in the `Shared` project. 81 | 2. Generate the gRPC client and server code using `dotnet-grpc` tools. 82 | 3. Implement the service in the `Server` project. 83 | 4. Inject and use the generated client in the `Client` project. 84 | 5. Define and implement new requests and handlers for your custom MediatR setup as needed. 85 | 86 | ## Contributing 87 | 88 | Contributions are welcome! Please fork the repository and create a pull request with your changes. Ensure your code follows the project's coding standards and includes appropriate tests. 89 | 90 | ### Steps to Contribute 91 | 92 | 1. Fork the repository. 93 | 2. Create a new branch for your feature or bugfix. 94 | 3. Make your changes and commit them. 95 | 4. Push your changes to your fork. 96 | 5. Create a pull request to the main repository. 97 | 98 | ## Acknowledgements 99 | 100 | - The .NET Foundation for providing a robust platform. 101 | - The gRPC contributors for creating a powerful RPC framework. 102 | - The Blazor community for continuous support and contributions. 103 | - The MediatR community for inspiring the custom implementation pattern. 104 | 105 | ## Connect with Me 106 | 107 | [![LinkedIn](https://img.shields.io/badge/LinkedIn-Profile-blue)](https://www.linkedin.com/in/spyros-ponaris-913a6937/) 108 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd --------------------------------------------------------------------------------