GetRefreshTokenModel(string refreshToken)
29 | {
30 | return authRepository.GetRefreshTokenModel(refreshToken);
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/BlazorApp.Web/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 | public HttpContext? HttpContext { get; set; }
30 |
31 | private string? requestId;
32 | private bool ShowRequestId => !string.IsNullOrEmpty(requestId);
33 |
34 | protected override void OnInitialized()
35 | {
36 | requestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/BaseComponents/CultureSelector.razor:
--------------------------------------------------------------------------------
1 | @using System.Globalization
2 | @inject IJSRuntime JS
3 | @inject NavigationManager Navigation
4 |
5 |
6 | @foreach (var culture in supportedCultures)
7 | {
8 | @culture.DisplayName
9 | }
10 |
11 |
12 | @code
13 | {
14 | private CultureInfo[] supportedCultures = new[]
15 | {
16 | new CultureInfo("en-US"),
17 | new CultureInfo("fr-FR"),
18 | };
19 |
20 | private CultureInfo selectedCulture;
21 |
22 | protected override void OnInitialized()
23 | {
24 | selectedCulture = CultureInfo.CurrentCulture;
25 | }
26 |
27 | private void ApplySelectedCulture()
28 | {
29 | if (CultureInfo.CurrentCulture != selectedCulture)
30 | {
31 | var uri = new Uri(Navigation.Uri)
32 | .GetComponents(UriComponents.PathAndQuery, UriFormat.Unescaped);
33 | var cultureEscaped = Uri.EscapeDataString(selectedCulture.Name);
34 | var uriEscaped = Uri.EscapeDataString(uri);
35 |
36 | Navigation.NavigateTo(
37 | $"Culture/Set?culture={cultureEscaped}&redirectUri={uriEscaped}",
38 | forceLoad: true);
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CRUD App with Blazor Web App and Web API (.NET 8)
2 |
3 | ## Overview
4 | This project is a CRUD (Create, Read, Update, Delete) application built using the latest .NET 8 technologies. It features a **Blazor Web App** as the frontend and a **Web API** as the backend, with **Entity Framework Core (EF Core) Code-First** approach for database management using **SQL Server**. It uses **.NET Aspire** for cloud-native development and application orchestration.
5 |
6 | ## Technologies Used
7 | - **Frontend**: Blazor Web App (.NET 8)
8 | - **Backend**: .NET 8 Web API
9 | - **Database**: SQL Server
10 | - **ORM**: Entity Framework Core (Code-First Approach)
11 | - **Authentication & Authorization**: JWT Token
12 | - **Dependency Injection**: Built-in DI in .NET Core
13 | - **API Documentation**: Swagger (Swashbuckle)
14 | - **Cloud-Native Development**: .NET Aspire
15 |
16 | ## Features
17 | - Perform CRUD operations on entities (e.g., Users, Products, etc.).
18 | - Blazor Web App consumes Web API endpoints.
19 | - SQL Server is used for data storage.
20 | - Layer architecture following best practices.
21 | - EF Core Code-First for database schema management.
22 | - Exception handling and logging.
23 | - Cache Service
24 |
25 | ## License
26 | This project is licensed under the MIT License
27 |
28 | ## Contact
29 | For any inquiries, feel free to reach out via email or open an issue in the repository.
30 | Email: dosehieu@gmail.com
31 |
32 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Pages/Product/CreateProduct.razor:
--------------------------------------------------------------------------------
1 | @page "/product/create"
2 | CreateProduct
3 | @rendermode InteractiveServer
4 |
5 |
6 |
7 |
8 |
9 |
10 | PrductName
11 |
12 |
13 |
14 |
15 | Quantity
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Price
24 |
25 |
26 |
27 |
28 | Description
29 |
30 |
31 |
32 |
33 |
Save
34 |
35 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Pages/Login/Login.razor:
--------------------------------------------------------------------------------
1 | @page "/login"
2 | @using BlazorApp.Model.Models
3 | @using BlazorApp.Web.Authentication
4 | @using BlazorApp.Web.Components.Layout
5 | @using Microsoft.AspNetCore.Components.Authorization
6 | @layout EmptyLayout
7 | @inject ApiClient ApiClient
8 | @inject NavigationManager Navigation
9 | @inject AuthenticationStateProvider AuthStateProvider
10 |
11 |
12 |
13 |
14 |
15 | Username
16 |
17 |
18 |
19 | Password
20 |
21 |
22 | Login
23 |
24 |
25 |
26 | @code {
27 | private LoginModel loginModel = new LoginModel();
28 | private async Task HandleLogin()
29 | {
30 | var res = await ApiClient.PostAsync("/api/auth/login", loginModel);
31 | if (res != null && res.Token != null)
32 | {
33 | await ((CustomAuthStateProvider)AuthStateProvider).MarkUserAsAuthenticated(res);
34 | Navigation.NavigateTo("/");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Dockerfile:
--------------------------------------------------------------------------------
1 | # See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | # This stage is used when running from VS in fast mode (Default for Debug configuration)
4 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
5 | USER $APP_UID
6 | WORKDIR /app
7 |
8 | # This stage is used to build the service project
9 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
10 | ARG BUILD_CONFIGURATION=Release
11 | WORKDIR /src
12 | COPY ["BlazorApp.Web/BlazorApp.Web.csproj", "BlazorApp.Web/"]
13 | COPY ["BlazorApp.Common/BlazorApp.Common.csproj", "BlazorApp.Common/"]
14 | COPY ["BlazorApp.Model/BlazorApp.Model.csproj", "BlazorApp.Model/"]
15 | COPY ["BlazorApp.ServiceDefaults/BlazorApp.ServiceDefaults.csproj", "BlazorApp.ServiceDefaults/"]
16 | RUN dotnet restore "./BlazorApp.Web/BlazorApp.Web.csproj"
17 | COPY . .
18 | WORKDIR "/src/BlazorApp.Web"
19 | RUN dotnet build "./BlazorApp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/build
20 |
21 | # This stage is used to publish the service project to be copied to the final stage
22 | FROM build AS publish
23 | ARG BUILD_CONFIGURATION=Release
24 | RUN dotnet publish "./BlazorApp.Web.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
25 |
26 | # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
27 | FROM base AS final
28 | WORKDIR /app
29 | COPY --from=publish /app/publish .
30 | ENTRYPOINT ["dotnet", "BlazorApp.Web.dll"]
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Pages/Product/UpdateProduct.razor:
--------------------------------------------------------------------------------
1 | @page "/product/update/{ID:int}"
2 | CreateProduct
3 | @rendermode InteractiveServer
4 |
5 |
6 |
7 |
8 |
9 |
10 | PrductName
11 |
12 |
13 |
14 |
15 | Quantity
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Price
24 |
25 |
26 |
27 |
28 | Description
29 |
30 |
31 |
32 |
33 |
Save
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Pages/Product/UpdateProduct.razor.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Model.Entities;
2 | using BlazorApp.Model.Models;
3 | using Blazored.Toast.Services;
4 | using Microsoft.AspNetCore.Components;
5 | using Newtonsoft.Json;
6 |
7 | namespace BlazorApp.Web.Components.Pages.Product
8 | {
9 | public partial class UpdateProduct : ComponentBase
10 | {
11 | [Parameter]
12 | public int ID { get; set; }
13 | public ProductModel Model { get; set; } = new();
14 |
15 | [Inject]
16 | private ApiClient ApiClient { get; set; }
17 | [Inject]
18 | private IToastService ToastService { get; set; }
19 | [Inject]
20 | private NavigationManager NavigationManager { get; set; }
21 |
22 | protected override async Task OnInitializedAsync()
23 | {
24 | await base.OnInitializedAsync();
25 | var res = await ApiClient.GetFromJsonAsync($"/api/Product/{ID}");
26 | if (res != null && res.Success)
27 | {
28 | Model = JsonConvert.DeserializeObject(res.Data.ToString());
29 | }
30 | }
31 |
32 | public async Task Submit()
33 | {
34 | var res = await ApiClient.PutAsync($"/api/Product/{ID}", Model);
35 | if (res != null && res.Success)
36 | {
37 | ToastService.ShowSuccess("Update product successfully");
38 | NavigationManager.NavigateTo("/product");
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/BlazorApp.ApiService/Dockerfile:
--------------------------------------------------------------------------------
1 | # See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging.
2 |
3 | # This stage is used when running from VS in fast mode (Default for Debug configuration)
4 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
5 | USER $APP_UID
6 | WORKDIR /app
7 |
8 | # This stage is used to build the service project
9 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
10 | ARG BUILD_CONFIGURATION=Release
11 | WORKDIR /src
12 | COPY ["BlazorApp.ApiService/BlazorApp.ApiService.csproj", "BlazorApp.ApiService/"]
13 | COPY ["BlazorApp.BL/BlazorApp.BL.csproj", "BlazorApp.BL/"]
14 | COPY ["BlazorApp.Common/BlazorApp.Common.csproj", "BlazorApp.Common/"]
15 | COPY ["BlazorApp.Database/BlazorApp.Database.csproj", "BlazorApp.Database/"]
16 | COPY ["BlazorApp.Model/BlazorApp.Model.csproj", "BlazorApp.Model/"]
17 | COPY ["BlazorApp.ServiceDefaults/BlazorApp.ServiceDefaults.csproj", "BlazorApp.ServiceDefaults/"]
18 | RUN dotnet restore "./BlazorApp.ApiService/BlazorApp.ApiService.csproj"
19 | COPY . .
20 | WORKDIR "/src/BlazorApp.ApiService"
21 | RUN dotnet build "./BlazorApp.ApiService.csproj" -c $BUILD_CONFIGURATION -o /app/build
22 |
23 | # This stage is used to publish the service project to be copied to the final stage
24 | FROM build AS publish
25 | ARG BUILD_CONFIGURATION=Release
26 | RUN dotnet publish "./BlazorApp.ApiService.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
27 |
28 | # This stage is used in production or when running from VS in regular mode (Default when not using the Debug configuration)
29 | FROM base AS final
30 | WORKDIR /app
31 | COPY --from=publish /app/publish .
32 | ENTRYPOINT ["dotnet", "BlazorApp.ApiService.dll"]
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Pages/Product/IndexProduct.razor.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Common.Resources;
2 | using BlazorApp.Model.Entities;
3 | using BlazorApp.Model.Models;
4 | using BlazorApp.Web.Components.BaseComponents;
5 | using Blazored.Toast.Services;
6 | using Microsoft.AspNetCore.Components;
7 | using Microsoft.Extensions.Localization;
8 | using Newtonsoft.Json;
9 |
10 | namespace BlazorApp.Web.Components.Pages.Product
11 | {
12 | public partial class IndexProduct
13 | {
14 | [Inject]
15 | public ApiClient ApiClient { get; set; }
16 | public List ProductModels { get; set; }
17 | public AppModal Modal { get; set; }
18 | public int DeleteID { get; set; }
19 | [Inject]
20 | private IToastService ToastService { get; set; }
21 | [Inject]
22 | public IStringLocalizer Localizers { get; set; }
23 |
24 |
25 | protected override async Task OnInitializedAsync()
26 | {
27 | await base.OnInitializedAsync();
28 | await LoadProduct();
29 | }
30 | protected async Task LoadProduct()
31 | {
32 | var res = await ApiClient.GetFromJsonAsync("/api/Product");
33 | if (res != null && res.Success)
34 | {
35 | ProductModels = JsonConvert.DeserializeObject>(res.Data.ToString());
36 | }
37 | }
38 | protected async Task HandleDelete()
39 | {
40 | var res = await ApiClient.DeleteAsync($"/api/Product/{DeleteID}");
41 | if (res != null && res.Success)
42 | {
43 | ToastService.ShowSuccess("Delete product successfully");
44 | await LoadProduct();
45 | Modal.Close();
46 | }
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/BlazorApp.ApiService/BlazorApp.ApiService.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | disable
7 | 8a0213ab-d48b-4de7-a328-572ef3efe146
8 | Linux
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | all
25 | runtime; build; native; contentfiles; analyzers; buildtransitive
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/BlazorApp.BL/Repositories/AuthRepository.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Database.Data;
2 | using BlazorApp.Model.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace BlazorApp.BL.Repositories
11 | {
12 | public interface IAuthRepository
13 | {
14 | Task GetUserByLogin(string username, string password);
15 | Task RemoveRefreshTokenByUserID(int userID);
16 | Task AddRefreshTokenModel(RefreshTokenModel refreshTokenModel);
17 | Task GetRefreshTokenModel(string refreshToken);
18 | }
19 | public class AuthRepository(AppDbContext dbContext) : IAuthRepository
20 | {
21 | public Task GetUserByLogin(string username, string password)
22 | {
23 | return dbContext.Users.Include(n => n.UserRoles).ThenInclude(n => n.Role).FirstOrDefaultAsync(n => n.Username == username && n.Password == password);
24 | }
25 | public async Task RemoveRefreshTokenByUserID(int userID)
26 | {
27 | var refreshToken = dbContext.RefreshTokens.FirstOrDefault(n => n.UserID == userID);
28 | if (refreshToken != null)
29 | {
30 | dbContext.RemoveRange(refreshToken);
31 | await dbContext.SaveChangesAsync();
32 | }
33 | }
34 | public async Task AddRefreshTokenModel(RefreshTokenModel refreshTokenModel)
35 | {
36 | await dbContext.RefreshTokens.AddAsync(refreshTokenModel);
37 | await dbContext.SaveChangesAsync();
38 | }
39 |
40 | public Task GetRefreshTokenModel(string refreshToken)
41 | {
42 | return dbContext.RefreshTokens.Include(n => n.User).ThenInclude(n => n.UserRoles).ThenInclude(n => n.Role).FirstOrDefaultAsync(n => n.RefreshToken == refreshToken);
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/BlazorApp.BL/Repositories/ProductRepository.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Database.Data;
2 | using BlazorApp.Model.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace BlazorApp.BL.Repositories
11 | {
12 | public interface IProductRepository
13 | {
14 | Task> GetProducts();
15 | Task GetProduct(int id);
16 | Task UpdateProduct(ProductModel productModel);
17 | Task CreateProduct(ProductModel productModel);
18 | Task ProductModelExists(int id);
19 | Task DeleteProduct(int id);
20 | }
21 | public class ProductRepository(AppDbContext dbContext) : IProductRepository
22 | {
23 | public Task> GetProducts()
24 | {
25 | return dbContext.Products.ToListAsync();
26 | }
27 |
28 | public Task GetProduct(int id)
29 | {
30 | return dbContext.Products.FirstOrDefaultAsync(n => n.ID == id);
31 | }
32 |
33 | public async Task CreateProduct(ProductModel productModel)
34 | {
35 | dbContext.Products.Add(productModel);
36 | await dbContext.SaveChangesAsync();
37 | return productModel;
38 | }
39 | public async Task UpdateProduct(ProductModel productModel)
40 | {
41 | dbContext.Entry(productModel).State = EntityState.Modified;
42 | await dbContext.SaveChangesAsync();
43 | }
44 | public Task ProductModelExists(int id)
45 | {
46 | return dbContext.Products.AnyAsync(e => e.ID == id);
47 | }
48 | public async Task DeleteProduct(int id)
49 | {
50 | var product = dbContext.Products.FirstOrDefault(n => n.ID == id);
51 | dbContext.Products.Remove(product);
52 | await dbContext.SaveChangesAsync();
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Program.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Web;
2 | using BlazorApp.Web.Authentication;
3 | using BlazorApp.Web.Components;
4 | using Blazored.Toast;
5 | using Microsoft.AspNetCore.Components.Authorization;
6 |
7 | var builder = WebApplication.CreateBuilder(args);
8 |
9 | // Add service defaults & Aspire components.
10 | builder.AddServiceDefaults();
11 |
12 | // Add services to the container.
13 | builder.Services.AddRazorComponents()
14 | .AddInteractiveServerComponents();
15 | builder.Services.AddBlazoredToast();
16 |
17 | builder.Services.AddAuthenticationCore();
18 | builder.Services.AddCascadingAuthenticationState();
19 |
20 | builder.Services.AddOutputCache();
21 | builder.Services.AddScoped();
22 |
23 | builder.Services.AddHttpClient(client =>
24 | {
25 | client.BaseAddress = new(Environment.GetEnvironmentVariable("API_BASE_URL") ?? "https+http://localhost:7304");
26 | });
27 |
28 | // Add localization services
29 | builder.Services.AddLocalization();
30 | builder.Services.AddControllers();
31 |
32 | var app = builder.Build();
33 |
34 | if (!app.Environment.IsDevelopment())
35 | {
36 | app.UseExceptionHandler("/Error", createScopeForErrors: true);
37 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
38 | app.UseHsts();
39 | }
40 | var supportedCultures = new[] { "en-US", "fr-FR" };
41 | var localizeoptions = new RequestLocalizationOptions()
42 | .SetDefaultCulture("en-US")
43 | .AddSupportedCultures(supportedCultures)
44 | .AddSupportedUICultures(supportedCultures);
45 |
46 | app.UseRequestLocalization(localizeoptions);
47 |
48 | app.UseHttpsRedirection();
49 |
50 | app.UseStaticFiles();
51 | app.UseAntiforgery();
52 |
53 | app.UseAuthentication();
54 | app.UseAuthorization();
55 |
56 | app.UseOutputCache();
57 |
58 | app.MapRazorComponents()
59 | .AddInteractiveServerRenderMode();
60 |
61 | app.MapDefaultEndpoints();
62 | app.MapControllers();
63 |
64 | app.Run();
65 |
--------------------------------------------------------------------------------
/BlazorApp.Web/wwwroot/app.css:
--------------------------------------------------------------------------------
1 | h1:focus {
2 | outline: none;
3 | }
4 |
5 | .valid.modified:not([type=checkbox]) {
6 | outline: 1px solid #26b050;
7 | }
8 |
9 | .invalid {
10 | outline: 1px solid #e50060;
11 | }
12 |
13 | .validation-message {
14 | color: #e50060;
15 | }
16 |
17 | .blazor-error-boundary {
18 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
19 | padding: 1rem 1rem 1rem 3.7rem;
20 | color: white;
21 | }
22 |
23 | .blazor-error-boundary::after {
24 | content: "An error has occurred."
25 | }
26 |
--------------------------------------------------------------------------------
/BlazorApp.Web/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 |
--------------------------------------------------------------------------------
/BlazorApp.BL/Services/ProductService.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.BL.Repositories;
2 | using BlazorApp.Model.Entities;
3 | using Microsoft.Extensions.Caching.Distributed;
4 | using Newtonsoft.Json;
5 |
6 | namespace BlazorApp.BL.Services
7 | {
8 | public interface IProductService
9 | {
10 | Task> GetProducts();
11 | Task GetProduct(int id);
12 | Task UpdateProduct(ProductModel productModel);
13 | Task CreateProduct(ProductModel productModel);
14 | Task ProductModelExists(int id);
15 | Task DeleteProduct(int id);
16 | }
17 | public class ProductService(IProductRepository productRepository, IDistributedCache cacheService) : IProductService
18 | {
19 | public async Task CreateProduct(ProductModel productModel)
20 | {
21 | var product = await productRepository.CreateProduct(productModel);
22 | await cacheService.RemoveAsync("list_products");
23 | return product;
24 | }
25 |
26 | public Task GetProduct(int id)
27 | {
28 | return productRepository.GetProduct(id);
29 | }
30 |
31 | public async Task> GetProducts()
32 | {
33 | var cacheValue = await cacheService.GetStringAsync("list_products");
34 | if (!string.IsNullOrEmpty(cacheValue))
35 | {
36 | return JsonConvert.DeserializeObject>(cacheValue);
37 | }
38 | var products = await productRepository.GetProducts();
39 | await cacheService.SetStringAsync("list_products", JsonConvert.SerializeObject(products));
40 | return products;
41 |
42 | }
43 |
44 | public Task ProductModelExists(int id)
45 | {
46 | return productRepository.ProductModelExists(id);
47 | }
48 |
49 | public async Task UpdateProduct(ProductModel productModel)
50 | {
51 | await productRepository.UpdateProduct(productModel);
52 | await cacheService.RemoveAsync("list_products");
53 | }
54 | public async Task DeleteProduct(int id)
55 | {
56 | await productRepository.DeleteProduct(id);
57 | await cacheService.RemoveAsync("list_products");
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Layout/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 | @using BlazorApp.Web.Authentication
3 | @using BlazorApp.Web.Components.Pages.Login
4 | @using Blazored.Toast.Configuration
5 | @using Microsoft.AspNetCore.Components.Authorization
6 | @inject AuthenticationStateProvider AuthStateProvider
7 | @inject NavigationManager Navigation
8 |
9 | @if (IsShowContent)
10 | {
11 |
12 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | Hello, @context.User.Identity?.Name!
22 |
23 |
24 | You're not authorized.
25 |
26 |
27 |
LogOut
28 |
29 |
30 |
31 | @Body
32 |
33 |
34 |
35 |
36 |
42 |
43 |
44 | An unhandled error has occurred.
45 |
Reload
46 |
🗙
47 |
48 | }
49 |
50 | @code{
51 | public bool IsShowContent { get; set; }
52 | protected override async Task OnInitializedAsync()
53 | {
54 | try
55 | {
56 | var authState = await ((CustomAuthStateProvider)AuthStateProvider).GetAuthenticationStateAsync();
57 | var user = authState.User;
58 |
59 | if (!user.Identity.IsAuthenticated)
60 | {
61 | Navigation.NavigateTo("/login");
62 | }
63 | else
64 | {
65 | IsShowContent = true;
66 | }
67 | }
68 | catch(Exception ex){
69 | Navigation.NavigateTo("/login", true);
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/BlazorApp.Web/Authentication/CustomAuthStateProvider.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Model.Models;
2 | using Microsoft.AspNetCore.Components.Authorization;
3 | using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
4 | using System.IdentityModel.Tokens.Jwt;
5 | using System.Security.Claims;
6 | using System.Security.Principal;
7 |
8 | namespace BlazorApp.Web.Authentication
9 | {
10 | public class CustomAuthStateProvider(ProtectedLocalStorage localStorage) : AuthenticationStateProvider
11 | {
12 | public async override Task GetAuthenticationStateAsync()
13 | {
14 | try
15 | {
16 | var sessionModel = (await localStorage.GetAsync("sessionState")).Value;
17 | var identity = sessionModel == null ? new ClaimsIdentity() : GetClaimsIdentity(sessionModel.Token);
18 | var user = new ClaimsPrincipal(identity);
19 | return new AuthenticationState(user);
20 | }
21 | catch(Exception ex)
22 | {
23 | await MarkUserAsLoggedOut();
24 | var identity = new ClaimsIdentity();
25 | var user = new ClaimsPrincipal(identity);
26 | return new AuthenticationState(user);
27 | }
28 | }
29 |
30 | public async Task MarkUserAsAuthenticated(LoginResponseModel model)
31 | {
32 | await localStorage.SetAsync("sessionState", model);
33 | var identity = GetClaimsIdentity(model.Token);
34 | var user = new ClaimsPrincipal(identity);
35 | NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
36 | }
37 |
38 | private ClaimsIdentity GetClaimsIdentity(string token)
39 | {
40 | var handler = new JwtSecurityTokenHandler();
41 | var jwtToken = handler.ReadJwtToken(token);
42 | var claims = jwtToken.Claims;
43 | return new ClaimsIdentity(claims, "jwt");
44 | }
45 |
46 | public async Task MarkUserAsLoggedOut()
47 | {
48 | await localStorage.DeleteAsync("sessionState");
49 | var identity = new ClaimsIdentity();
50 | var user = new ClaimsPrincipal(identity);
51 | NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(user)));
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/BlazorApp.Web/Components/Pages/Product/IndexProduct.razor:
--------------------------------------------------------------------------------
1 | @page "/product"
2 | @using Microsoft.AspNetCore.Authorization
3 | @using Microsoft.AspNetCore.Components.Authorization
4 | @using Microsoft.Extensions.Localization
5 |
6 |
7 |
8 | Not Authorized
9 |
10 |
11 | @Localizers["Title"]
12 | @if (ProductModels == null)
13 | {
14 | Loading ...
15 | }
16 | else
17 | {
18 | Create
19 |
20 |
21 |
22 | ProductName
23 | Quantity
24 | Price
25 | Description
26 | CreatedAt
27 | Action
28 |
29 |
30 |
31 | @foreach (var product in ProductModels)
32 | {
33 |
34 | @product.ProductName
35 | @product.Quantity
36 | @product.Price
37 | @product.Description
38 | @product.CreateAt.ToShortDateString()
39 |
40 | Update
41 |
42 |
43 | { DeleteID = product.ID; Modal.Open();}">Delete
44 |
45 |
46 |
47 |
48 | }
49 |
50 |
51 |
52 |
53 | Notification
54 |
55 | Do you sure want to delete this product?
56 |
57 |
58 | Yes
59 | Modal.Close()">Cancel
60 |
61 |
62 | }
63 |
64 |
65 |
--------------------------------------------------------------------------------
/BlazorApp.ApiService/Controllers/ProductController.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.BL.Services;
2 | using BlazorApp.Common.Resources;
3 | using BlazorApp.Model.Entities;
4 | using BlazorApp.Model.Models;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.Extensions.Localization;
7 |
8 | namespace BlazorApp.ApiService.Controllers
9 | {
10 | [Route("api/[controller]")]
11 | [ApiController]
12 | public class ProductController(IProductService productService, IStringLocalizer localizer, ILogger logger) : ControllerBase
13 | {
14 | [HttpGet]
15 | public async Task> GetProducts()
16 | {
17 | try
18 | {
19 | logger.LogInformation("Test log GetProducts");
20 | var products = await productService.GetProducts();
21 | foreach (var product in products)
22 | {
23 | product.Description = product.Description != null ? localizer[product.Description] : null;
24 | }
25 | return Ok(new BaseResponseModel { Success = true, Data = products });
26 | }
27 | catch (Exception ex)
28 | {
29 | logger.LogError(ex, "GetProducts error");
30 | return Ok(new BaseResponseModel { Success = false, ErrorMessage = ex.Message });
31 | }
32 | }
33 |
34 | [HttpPost]
35 | public async Task> CreateProduct(ProductModel productModel)
36 | {
37 | await productService.CreateProduct(productModel);
38 | return Ok(new BaseResponseModel { Success = true });
39 | }
40 |
41 | [HttpGet("{id}")]
42 | public async Task> GetProduct(int id)
43 | {
44 | var productModel = await productService.GetProduct(id);
45 |
46 | if (productModel == null)
47 | {
48 | return Ok(new BaseResponseModel { Success = false, ErrorMessage = "Not Found" });
49 | }
50 | return Ok(new BaseResponseModel { Success = true, Data = productModel });
51 | }
52 |
53 | [HttpPut("{id}")]
54 | public async Task UpdateProduct(int id, ProductModel productModel)
55 | {
56 | if (id != productModel.ID || !await productService.ProductModelExists(id))
57 | {
58 | return Ok(new BaseResponseModel { Success = false, ErrorMessage = "Bad request" });
59 | }
60 |
61 | await productService.UpdateProduct(productModel);
62 | return Ok(new BaseResponseModel { Success = true });
63 | }
64 |
65 | [HttpDelete("{id}")]
66 | public async Task DeleteProduct(int id)
67 | {
68 | if (!await productService.ProductModelExists(id))
69 | {
70 | return Ok(new BaseResponseModel { Success = false, ErrorMessage = "Not Found" });
71 | }
72 | await productService.DeleteProduct(id);
73 | return Ok(new BaseResponseModel { Success = true });
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/BlazorApp.Common/Resources/ProductTranslation.Designer.cs:
--------------------------------------------------------------------------------
1 | //------------------------------------------------------------------------------
2 | //
3 | // This code was generated by a tool.
4 | // Runtime Version:4.0.30319.42000
5 | //
6 | // Changes to this file may cause incorrect behavior and will be lost if
7 | // the code is regenerated.
8 | //
9 | //------------------------------------------------------------------------------
10 |
11 | namespace BlazorApp.Common.Resources {
12 | using System;
13 |
14 |
15 | ///
16 | /// A strongly-typed resource class, for looking up localized strings, etc.
17 | ///
18 | // This class was auto-generated by the StronglyTypedResourceBuilder
19 | // class via a tool like ResGen or Visual Studio.
20 | // To add or remove a member, edit your .ResX file then rerun ResGen
21 | // with the /str option, or rebuild your VS project.
22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
25 | public class ProductTranslation {
26 |
27 | private static global::System.Resources.ResourceManager resourceMan;
28 |
29 | private static global::System.Globalization.CultureInfo resourceCulture;
30 |
31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
32 | internal ProductTranslation() {
33 | }
34 |
35 | ///
36 | /// Returns the cached ResourceManager instance used by this class.
37 | ///
38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
39 | public static global::System.Resources.ResourceManager ResourceManager {
40 | get {
41 | if (object.ReferenceEquals(resourceMan, null)) {
42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("BlazorApp.Common.Resources.ProductTranslation", typeof(ProductTranslation).Assembly);
43 | resourceMan = temp;
44 | }
45 | return resourceMan;
46 | }
47 | }
48 |
49 | ///
50 | /// Overrides the current thread's CurrentUICulture property for all
51 | /// resource lookups using this strongly typed resource class.
52 | ///
53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
54 | public static global::System.Globalization.CultureInfo Culture {
55 | get {
56 | return resourceCulture;
57 | }
58 | set {
59 | resourceCulture = value;
60 | }
61 | }
62 |
63 | ///
64 | /// Looks up a localized string similar to Free Size.
65 | ///
66 | public static string FreeSize {
67 | get {
68 | return ResourceManager.GetString("FreeSize", resourceCulture);
69 | }
70 | }
71 |
72 | ///
73 | /// Looks up a localized string similar to List Products.
74 | ///
75 | public static string Title {
76 | get {
77 | return ResourceManager.GetString("Title", resourceCulture);
78 | }
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/BlazorApp.Web/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 {
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 {
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 {
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 a {
63 | color: #d7d7d7;
64 | border-radius: 4px;
65 | height: 3rem;
66 | display: flex;
67 | align-items: center;
68 | line-height: 3rem;
69 | }
70 |
71 | .nav-item ::deep a.active {
72 | background-color: rgba(255,255,255,0.37);
73 | color: white;
74 | }
75 |
76 | .nav-item ::deep a:hover {
77 | background-color: rgba(255,255,255,0.1);
78 | color: white;
79 | }
80 |
81 | .nav-scrollable {
82 | display: none;
83 | }
84 |
85 | .navbar-toggler:checked ~ .nav-scrollable {
86 | display: block;
87 | }
88 |
89 | @media (min-width: 641px) {
90 | .navbar-toggler {
91 | display: none;
92 | }
93 |
94 | .nav-scrollable {
95 | /* Never collapse the sidebar for wide screens */
96 | display: block;
97 |
98 | /* Allow sidebar to scroll for tall menus */
99 | height: calc(100vh - 3.5rem);
100 | overflow-y: auto;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/BlazorApp.ApiService/Controllers/AuthController.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.BL.Services;
2 | using BlazorApp.Model.Entities;
3 | using BlazorApp.Model.Models;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Mvc;
6 | using Microsoft.IdentityModel.Tokens;
7 | using System.IdentityModel.Tokens.Jwt;
8 | using System.Security.Claims;
9 | using System.Text;
10 |
11 | namespace BlazorApp.ApiService.Controllers
12 | {
13 | [Route("api/[controller]")]
14 | [ApiController]
15 | public class AuthController(IConfiguration configuration, IAuthService authService) : ControllerBase
16 | {
17 | [HttpPost("login")]
18 | public async Task> Login([FromBody] LoginModel loginModel)
19 | {
20 | var user = await authService.GetUserByLogin(loginModel.Username, loginModel.Password);
21 | if (user != null)
22 | {
23 | var token = GenerateJwtToken(user, isRefreshToken:false);
24 | var refreshToken = GenerateJwtToken(user, isRefreshToken: true);
25 |
26 | await authService.AddRefreshTokenModel(new RefreshTokenModel
27 | {
28 | RefreshToken = refreshToken,
29 | UserID = user.ID
30 | });
31 |
32 | return Ok(new LoginResponseModel {
33 | Token = token,
34 | RefreshToken = refreshToken,
35 | TokenExpired = DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds(),
36 | });
37 | }
38 | return null;
39 | }
40 | [HttpGet("loginByRefeshToken")]
41 | public async Task> LoginByRefeshToken(string refreshToken)
42 | {
43 | var refreshTokenModel = await authService.GetRefreshTokenModel(refreshToken);
44 | if (refreshTokenModel == null)
45 | {
46 | return StatusCode(StatusCodes.Status400BadRequest);
47 | }
48 |
49 | var newToken = GenerateJwtToken(refreshTokenModel.User, isRefreshToken: false);
50 | var newRefreshToken = GenerateJwtToken(refreshTokenModel.User, isRefreshToken: true);
51 |
52 | await authService.AddRefreshTokenModel(new RefreshTokenModel
53 | {
54 | RefreshToken = newRefreshToken,
55 | UserID = refreshTokenModel.UserID
56 | });
57 |
58 | return new LoginResponseModel
59 | {
60 | Token = newToken,
61 | TokenExpired = DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds(),
62 | RefreshToken = newRefreshToken,
63 | };
64 | }
65 |
66 | private string GenerateJwtToken(UserModel user, bool isRefreshToken)
67 | {
68 | var claims = new List()
69 | {
70 | new Claim(ClaimTypes.Name, user.Username),
71 | };
72 | claims.AddRange(user.UserRoles.Select(n => new Claim(ClaimTypes.Role, n.Role.RoleName)));
73 |
74 | string secret = configuration.GetValue($"Jwt:{(isRefreshToken ? "RefreshTokenSecret" : "Secret")}");
75 | var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret));
76 | var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
77 |
78 | var token = new JwtSecurityToken(
79 | issuer: "doseHieu",
80 | audience: "doseHieu",
81 | claims: claims,
82 | expires: DateTime.UtcNow.AddMinutes(isRefreshToken ? 24*60 : 30),
83 | signingCredentials: creds
84 | );
85 | return new JwtSecurityTokenHandler().WriteToken(token);
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/BlazorApp.Web/ApiClient.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.Model.Models;
2 | using BlazorApp.Web.Authentication;
3 | using Microsoft.AspNetCore.Components;
4 | using Microsoft.AspNetCore.Components.Authorization;
5 | using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
6 | using Microsoft.AspNetCore.Localization;
7 | using Newtonsoft.Json;
8 | using System.Globalization;
9 | using System.Net.Http.Headers;
10 |
11 | namespace BlazorApp.Web;
12 |
13 | public class ApiClient(HttpClient httpClient, ProtectedLocalStorage localStorage, NavigationManager navigationManager, AuthenticationStateProvider authStateProvider)
14 | {
15 | public async Task SetAuthorizeHeader()
16 | {
17 | try
18 | {
19 | var sessionState = (await localStorage.GetAsync("sessionState")).Value;
20 | if (sessionState != null && !string.IsNullOrEmpty(sessionState.Token))
21 | {
22 | if (sessionState.TokenExpired < DateTimeOffset.UtcNow.ToUnixTimeSeconds())
23 | {
24 | await ((CustomAuthStateProvider)authStateProvider).MarkUserAsLoggedOut();
25 | navigationManager.NavigateTo("/login");
26 | }
27 | else if (sessionState.TokenExpired < DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds())
28 | {
29 | var res = await httpClient.GetFromJsonAsync($"/api/auth/loginByRefeshToken?refreshToken={sessionState.RefreshToken}");
30 | if (res != null)
31 | {
32 | await ((CustomAuthStateProvider)authStateProvider).MarkUserAsAuthenticated(res);
33 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", res.Token);
34 | }
35 | else
36 | {
37 | await ((CustomAuthStateProvider)authStateProvider).MarkUserAsLoggedOut();
38 | navigationManager.NavigateTo("/login");
39 | }
40 | }
41 | else
42 | {
43 | httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", sessionState.Token);
44 | }
45 |
46 | var requestCulture = new RequestCulture(
47 | CultureInfo.CurrentCulture,
48 | CultureInfo.CurrentUICulture
49 | );
50 | var cultureCookieValue = CookieRequestCultureProvider.MakeCookieValue(requestCulture);
51 |
52 | httpClient.DefaultRequestHeaders.Add("Cookie", $"{CookieRequestCultureProvider.DefaultCookieName}={cultureCookieValue}");
53 | }
54 | }
55 | catch( Exception ex )
56 | {
57 | navigationManager.NavigateTo("/login");
58 | }
59 | }
60 | public async Task GetFromJsonAsync(string path)
61 | {
62 | await SetAuthorizeHeader();
63 | return await httpClient.GetFromJsonAsync(path);
64 | }
65 | public async Task PostAsync(string path, T2 postModel)
66 | {
67 | await SetAuthorizeHeader();
68 |
69 | var res = await httpClient.PostAsJsonAsync(path, postModel);
70 | if (res != null && res.IsSuccessStatusCode)
71 | {
72 | return JsonConvert.DeserializeObject(await res.Content.ReadAsStringAsync());
73 | }
74 | return default;
75 | }
76 | public async Task PutAsync(string path, T2 postModel)
77 | {
78 | await SetAuthorizeHeader();
79 | var res = await httpClient.PutAsJsonAsync(path, postModel);
80 | if (res != null && res.IsSuccessStatusCode)
81 | {
82 | return JsonConvert.DeserializeObject(await res.Content.ReadAsStringAsync());
83 | }
84 | return default;
85 | }
86 | public async Task DeleteAsync(string path)
87 | {
88 | await SetAuthorizeHeader();
89 | return await httpClient.DeleteFromJsonAsync(path);
90 | }
91 | }
92 |
93 |
--------------------------------------------------------------------------------
/BlazorApp.ApiService/Program.cs:
--------------------------------------------------------------------------------
1 | using BlazorApp.BL.Repositories;
2 | using BlazorApp.BL.Services;
3 | using BlazorApp.Database.Data;
4 | using Microsoft.AspNetCore.Authentication.JwtBearer;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.IdentityModel.Tokens;
7 | using Microsoft.OpenApi.Models;
8 | using NLog;
9 | using NLog.Web;
10 | using System.Text;
11 |
12 |
13 | var logger = LogManager.Setup().LoadConfigurationFromAppSettings()
14 | .GetCurrentClassLogger();
15 | logger.Debug("init main");
16 | try
17 | {
18 |
19 | var builder = WebApplication.CreateBuilder(args);
20 |
21 | // Add service defaults & Aspire components.
22 | builder.AddServiceDefaults();
23 | builder.Services.AddLocalization();
24 |
25 | builder.Services.AddControllers();
26 | builder.Services.AddEndpointsApiExplorer();
27 | builder.Services.AddSwaggerGen(c =>
28 | {
29 | c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
30 | {
31 | In = ParameterLocation.Header,
32 | Description = "Please insert JWT with Bearer into field",
33 | Name = "Authorization",
34 | Type = SecuritySchemeType.ApiKey
35 | });
36 | c.AddSecurityRequirement(new OpenApiSecurityRequirement
37 | {
38 | {
39 | new OpenApiSecurityScheme
40 | {
41 | Reference = new OpenApiReference
42 | {
43 | Type = ReferenceType.SecurityScheme,
44 | Id = "Bearer"
45 | }
46 | },
47 | Array.Empty()
48 | }
49 | });
50 | });
51 |
52 | builder.Services.AddDbContext(options =>
53 | {
54 | options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
55 | });
56 |
57 | var secret = builder.Configuration.GetValue("Jwt:Secret");
58 | builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
59 | {
60 | options.TokenValidationParameters = new TokenValidationParameters
61 | {
62 | ValidateIssuer = true,
63 | ValidateAudience = true,
64 | ValidateLifetime = true,
65 | ValidateIssuerSigningKey = true,
66 | ValidIssuer = "doseHieu",
67 | ValidAudience = "doseHieu",
68 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret))
69 | };
70 | });
71 | builder.Services.AddAuthorization();
72 | builder.AddRedisDistributedCache("cache");
73 |
74 |
75 | builder.Services.AddScoped();
76 | builder.Services.AddScoped();
77 | builder.Services.AddScoped();
78 | builder.Services.AddScoped();
79 |
80 | // NLog: Setup NLog for Dependency injection
81 | builder.Logging.ClearProviders();
82 | builder.Logging.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
83 | builder.Host.UseNLog();
84 |
85 |
86 | // Add services to the container.
87 | builder.Services.AddProblemDetails();
88 |
89 | var app = builder.Build();
90 |
91 |
92 | // Configure the HTTP request pipeline.
93 | app.UseExceptionHandler();
94 | app.UseAuthentication();
95 | app.UseAuthorization();
96 |
97 | var supportedCultures = new[] { "en-US", "fr-FR" };
98 | var localizeoptions = new RequestLocalizationOptions()
99 | .SetDefaultCulture("en-US")
100 | .AddSupportedCultures(supportedCultures)
101 | .AddSupportedUICultures(supportedCultures);
102 |
103 | app.UseRequestLocalization(localizeoptions);
104 |
105 | app.MapControllers();
106 |
107 | if (app.Environment.IsDevelopment())
108 | {
109 | app.UseSwagger();
110 | app.UseSwaggerUI();
111 | }
112 |
113 | app.MapDefaultEndpoints();
114 |
115 | app.Run();
116 |
117 | }
118 | catch (Exception exception)
119 | {
120 | // NLog: catch setup errors
121 | logger.Error(exception, "Stopped program because of exception");
122 | throw;
123 | }
124 | finally
125 | {
126 | // Ensure to flush and stop internal timers/threads before application-exit (Avoid segmentation fault on Linux)
127 | LogManager.Shutdown();
128 | }
--------------------------------------------------------------------------------
/BlazorApp.ServiceDefaults/Extensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Builder;
2 | using Microsoft.AspNetCore.Diagnostics.HealthChecks;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Diagnostics.HealthChecks;
5 | using Microsoft.Extensions.Logging;
6 | using OpenTelemetry;
7 | using OpenTelemetry.Metrics;
8 | using OpenTelemetry.Trace;
9 |
10 | namespace Microsoft.Extensions.Hosting;
11 |
12 | // Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
13 | // This project should be referenced by each service project in your solution.
14 | // To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
15 | public static class Extensions
16 | {
17 | public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder)
18 | {
19 | builder.ConfigureOpenTelemetry();
20 |
21 | builder.AddDefaultHealthChecks();
22 |
23 | builder.Services.AddServiceDiscovery();
24 |
25 | builder.Services.ConfigureHttpClientDefaults(http =>
26 | {
27 | // Turn on resilience by default
28 | http.AddStandardResilienceHandler();
29 |
30 | // Turn on service discovery by default
31 | http.AddServiceDiscovery();
32 | });
33 |
34 | return builder;
35 | }
36 |
37 | public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder)
38 | {
39 | builder.Logging.AddOpenTelemetry(logging =>
40 | {
41 | logging.IncludeFormattedMessage = true;
42 | logging.IncludeScopes = true;
43 | });
44 |
45 | builder.Services.AddOpenTelemetry()
46 | .WithMetrics(metrics =>
47 | {
48 | metrics.AddAspNetCoreInstrumentation()
49 | .AddHttpClientInstrumentation()
50 | .AddRuntimeInstrumentation();
51 | })
52 | .WithTracing(tracing =>
53 | {
54 | tracing.AddAspNetCoreInstrumentation()
55 | // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
56 | //.AddGrpcClientInstrumentation()
57 | .AddHttpClientInstrumentation();
58 | });
59 |
60 | builder.AddOpenTelemetryExporters();
61 |
62 | return builder;
63 | }
64 |
65 | private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder)
66 | {
67 | var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
68 |
69 | if (useOtlpExporter)
70 | {
71 | builder.Services.AddOpenTelemetry().UseOtlpExporter();
72 | }
73 |
74 | // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
75 | //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
76 | //{
77 | // builder.Services.AddOpenTelemetry()
78 | // .UseAzureMonitor();
79 | //}
80 |
81 | return builder;
82 | }
83 |
84 | public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder)
85 | {
86 | builder.Services.AddHealthChecks()
87 | // Add a default liveness check to ensure app is responsive
88 | .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
89 |
90 | return builder;
91 | }
92 |
93 | public static WebApplication MapDefaultEndpoints(this WebApplication app)
94 | {
95 | // Adding health checks endpoints to applications in non-development environments has security implications.
96 | // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
97 | if (app.Environment.IsDevelopment())
98 | {
99 | // All health checks must pass for app to be considered ready to accept traffic after starting
100 | app.MapHealthChecks("/health");
101 |
102 | // Only health checks tagged with the "live" tag must pass for app to be considered alive
103 | app.MapHealthChecks("/alive", new HealthCheckOptions
104 | {
105 | Predicate = r => r.Tags.Contains("live")
106 | });
107 | }
108 |
109 | return app;
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/BlazorApp.sln:
--------------------------------------------------------------------------------
1 | Microsoft Visual Studio Solution File, Format Version 12.00
2 | # Visual Studio Version 17
3 | VisualStudioVersion = 17.10.34916.146
4 | MinimumVisualStudioVersion = 10.0.40219.1
5 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.AppHost", "BlazorApp.AppHost\BlazorApp.AppHost.csproj", "{2AB9EF20-2DE2-495A-9D35-ED03FED00C90}"
6 | EndProject
7 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.ServiceDefaults", "BlazorApp.ServiceDefaults\BlazorApp.ServiceDefaults.csproj", "{C15FD811-DCD5-41E8-807A-9C5085D45378}"
8 | EndProject
9 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.ApiService", "BlazorApp.ApiService\BlazorApp.ApiService.csproj", "{91D16708-91D9-458B-8A46-A39583DB85EA}"
10 | EndProject
11 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.Web", "BlazorApp.Web\BlazorApp.Web.csproj", "{891BA792-237F-437A-813E-C8FA7F4D27FE}"
12 | EndProject
13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.BL", "BlazorApp.BL\BlazorApp.BL.csproj", "{E9EE618A-8D4F-44F4-A576-C777C5BB5CB8}"
14 | EndProject
15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.Model", "BlazorApp.Model\BlazorApp.Model.csproj", "{D0EEE002-864C-4B7B-A594-DA02BC5A3491}"
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BlazorApp.Database", "BlazorApp.Database\BlazorApp.Database.csproj", "{AEF0ED70-D5D3-48B4-A62E-B7413F73A814}"
18 | EndProject
19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BlazorApp.Common", "BlazorApp.Common\BlazorApp.Common.csproj", "{A9A8EFDF-DC15-4880-85CD-DAE76D877BC2}"
20 | EndProject
21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F0DFD522-3B68-42D5-9DA5-338631C6866C}"
22 | ProjectSection(SolutionItems) = preProject
23 | docker-compose.yml = docker-compose.yml
24 | EndProjectSection
25 | EndProject
26 | Global
27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
28 | Debug|Any CPU = Debug|Any CPU
29 | Release|Any CPU = Release|Any CPU
30 | EndGlobalSection
31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
32 | {2AB9EF20-2DE2-495A-9D35-ED03FED00C90}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {2AB9EF20-2DE2-495A-9D35-ED03FED00C90}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {2AB9EF20-2DE2-495A-9D35-ED03FED00C90}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {2AB9EF20-2DE2-495A-9D35-ED03FED00C90}.Release|Any CPU.Build.0 = Release|Any CPU
36 | {C15FD811-DCD5-41E8-807A-9C5085D45378}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
37 | {C15FD811-DCD5-41E8-807A-9C5085D45378}.Debug|Any CPU.Build.0 = Debug|Any CPU
38 | {C15FD811-DCD5-41E8-807A-9C5085D45378}.Release|Any CPU.ActiveCfg = Release|Any CPU
39 | {C15FD811-DCD5-41E8-807A-9C5085D45378}.Release|Any CPU.Build.0 = Release|Any CPU
40 | {91D16708-91D9-458B-8A46-A39583DB85EA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {91D16708-91D9-458B-8A46-A39583DB85EA}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {91D16708-91D9-458B-8A46-A39583DB85EA}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {91D16708-91D9-458B-8A46-A39583DB85EA}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {891BA792-237F-437A-813E-C8FA7F4D27FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {891BA792-237F-437A-813E-C8FA7F4D27FE}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {891BA792-237F-437A-813E-C8FA7F4D27FE}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {891BA792-237F-437A-813E-C8FA7F4D27FE}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {E9EE618A-8D4F-44F4-A576-C777C5BB5CB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {E9EE618A-8D4F-44F4-A576-C777C5BB5CB8}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {E9EE618A-8D4F-44F4-A576-C777C5BB5CB8}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {E9EE618A-8D4F-44F4-A576-C777C5BB5CB8}.Release|Any CPU.Build.0 = Release|Any CPU
52 | {D0EEE002-864C-4B7B-A594-DA02BC5A3491}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {D0EEE002-864C-4B7B-A594-DA02BC5A3491}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {D0EEE002-864C-4B7B-A594-DA02BC5A3491}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {D0EEE002-864C-4B7B-A594-DA02BC5A3491}.Release|Any CPU.Build.0 = Release|Any CPU
56 | {AEF0ED70-D5D3-48B4-A62E-B7413F73A814}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 | {AEF0ED70-D5D3-48B4-A62E-B7413F73A814}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 | {AEF0ED70-D5D3-48B4-A62E-B7413F73A814}.Release|Any CPU.ActiveCfg = Release|Any CPU
59 | {AEF0ED70-D5D3-48B4-A62E-B7413F73A814}.Release|Any CPU.Build.0 = Release|Any CPU
60 | {A9A8EFDF-DC15-4880-85CD-DAE76D877BC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61 | {A9A8EFDF-DC15-4880-85CD-DAE76D877BC2}.Debug|Any CPU.Build.0 = Debug|Any CPU
62 | {A9A8EFDF-DC15-4880-85CD-DAE76D877BC2}.Release|Any CPU.ActiveCfg = Release|Any CPU
63 | {A9A8EFDF-DC15-4880-85CD-DAE76D877BC2}.Release|Any CPU.Build.0 = Release|Any CPU
64 | EndGlobalSection
65 | GlobalSection(SolutionProperties) = preSolution
66 | HideSolutionNode = FALSE
67 | EndGlobalSection
68 | GlobalSection(ExtensibilityGlobals) = postSolution
69 | SolutionGuid = {2E306392-D5C2-4F03-8AD8-44E3F41BE9A3}
70 | EndGlobalSection
71 | EndGlobal
72 |
--------------------------------------------------------------------------------
/BlazorApp.Common/Resources/ProductTranslation.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | List Products
122 |
123 |
124 | Free Size
125 |
126 |
--------------------------------------------------------------------------------
/BlazorApp.Common/Resources/ProductTranslation.fr.resx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | text/microsoft-resx
110 |
111 |
112 | 2.0
113 |
114 |
115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
116 |
117 |
118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
119 |
120 |
121 | Liste des produits
122 |
123 |
124 | Taille libre
125 |
126 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------