From 0e7c1d7f81654d6cc623e62ebd45c939288fefc4 Mon Sep 17 00:00:00 2001 From: "Amir.H Khademi" Date: Fri, 6 Dec 2024 17:37:40 +0330 Subject: [PATCH] Refactor and enhance product and order handling - Updated `DiscountActionDialogBoxViewModel` and `FastProductCreateDialogBoxViewModel` to create command objects directly from properties and improved error handling. - Added meta tag management UI and logic in `ProductActionDialogBox.razor` and `ProductActionDialogBoxViewModel`. - Increased max file read stream size to 8 MB in `StorageDialogBoxViewModel`. - Incremented `AssemblyVersion` and `FileVersion` to `1.7.20.34` in `Netina.AdminPanel.PWA.csproj`. - Updated `BrandsPage.razor` and `BrandsPageViewModel` for pagination and service injection. - Updated `CategoriesPageViewModel` to create command objects directly from properties. - Updated `ProductsPage.razor` for service injection and added a button for product details. - Updated `ICrudApiRest` and `ICrudDtoApiRest` interfaces to use generic `Create` methods. - Updated `appsettings.Development.json` for `StorageBaseUrl` and commented out `IsShop`. - Added new project `AppHost.csproj` targeting .NET 8.0 with Aspire hosting. - Added new `appsettings.Development.json` and `appsettings.json` for logging. - Added new `Program.cs` to create and run a distributed application. - Added new `launchSettings.json` for application launch settings. - Added `Extensions.cs` for common .NET Aspire services. - Added new project `ServiceDefaults.csproj` for shared service defaults. - Introduced `ProductMetaTag` class and related migration for meta tag handling. - Updated `OrderController.cs` for additional authorization requirements. - Updated target frameworks to `net8.0` in various projects. - Enhanced `SiteMapService.cs` to include brand site maps. - Added new properties to DTOs for customer and meta tag handling. - Enhanced `Product` class with meta tag management methods. - Refactored `OrderMapper.g.cs` and `ProductMapper.g.cs` for improved mapping logic. - Enhanced command handlers to manage meta tags. - Added `ICurrentUserService` for user permissions in query handlers. - Refactored `StorageService.cs` for paginated storage file fetching. --- AppHost/AppHost.csproj | 20 +++ AppHost/Program.cs | 3 + AppHost/Properties/launchSettings.json | 29 +++++ AppHost/appsettings.Development.json | 8 ++ AppHost/appsettings.json | 9 ++ .../Dialogs/DiscountActionDialogBox.razor.cs | 59 ++++++++- .../FastProductCreateDialogBox.razor.cs | 35 ++++-- .../Dialogs/ProductActionDialogBox.razor | 55 +++++++- .../Dialogs/ProductActionDialogBox.razor.cs | 30 ++++- .../Dialogs/StorageDialogBox.razor.cs | 2 +- .../Netina.AdminPanel.PWA.csproj | 4 +- Netina.AdminPanel.PWA/Pages/BrandsPage.razor | 28 +++-- .../Pages/BrandsPage.razor.cs | 6 +- .../Pages/CategoriesPage.razor.cs | 8 +- .../Pages/ProductsPage.razor | 16 +++ .../Services/RestServices/ICrudApiRest.cs | 2 +- .../wwwroot/appsettings.Development.json | 4 +- ServiceDefaults/Extensions.cs | 118 ++++++++++++++++++ ServiceDefaults/ServiceDefaults.csproj | 22 ++++ 19 files changed, 418 insertions(+), 40 deletions(-) create mode 100644 AppHost/AppHost.csproj create mode 100644 AppHost/Program.cs create mode 100644 AppHost/Properties/launchSettings.json create mode 100644 AppHost/appsettings.Development.json create mode 100644 AppHost/appsettings.json create mode 100644 ServiceDefaults/Extensions.cs create mode 100644 ServiceDefaults/ServiceDefaults.csproj diff --git a/AppHost/AppHost.csproj b/AppHost/AppHost.csproj new file mode 100644 index 0000000..53c3824 --- /dev/null +++ b/AppHost/AppHost.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + true + 045801e7-58a1-4ee6-9d28-93c23b5dcd6b + + + + + + + + + + + diff --git a/AppHost/Program.cs b/AppHost/Program.cs new file mode 100644 index 0000000..c62c3a0 --- /dev/null +++ b/AppHost/Program.cs @@ -0,0 +1,3 @@ +var builder = DistributedApplication.CreateBuilder(args); + +builder.Build().Run(); diff --git a/AppHost/Properties/launchSettings.json b/AppHost/Properties/launchSettings.json new file mode 100644 index 0000000..c69c691 --- /dev/null +++ b/AppHost/Properties/launchSettings.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:17057;http://localhost:15006", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21274", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22232" + } + }, + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "http://localhost:15006", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19034", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20044" + } + } + } +} diff --git a/AppHost/appsettings.Development.json b/AppHost/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/AppHost/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/AppHost/appsettings.json b/AppHost/appsettings.json new file mode 100644 index 0000000..31c092a --- /dev/null +++ b/AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/Netina.AdminPanel.PWA/Dialogs/DiscountActionDialogBox.razor.cs b/Netina.AdminPanel.PWA/Dialogs/DiscountActionDialogBox.razor.cs index 7515bd7..5c4444e 100644 --- a/Netina.AdminPanel.PWA/Dialogs/DiscountActionDialogBox.razor.cs +++ b/Netina.AdminPanel.PWA/Dialogs/DiscountActionDialogBox.razor.cs @@ -194,16 +194,42 @@ public class DiscountActionDialogBoxViewModel : BaseViewModel if(StartDate != null) PageDto.StartDate = StartDate.Value; - var request = PageDto.Adapt(); - await _restWrapper.CrudApiRest(Address.DiscountController).Create(request, token); + var request = new CreateDiscountCommand(PageDto.Code, + PageDto.Description, + PageDto.DiscountPercent, + PageDto.DiscountAmount, + PageDto.HasCode, + PageDto.AmountType, + PageDto.Type, + PageDto.Count, + PageDto.StartDate, + PageDto.ExpireDate, + PageDto.Immortal, + PageDto.PriceFloor, + PageDto.HasPriceFloor, + PageDto.PriceCeiling, + PageDto.HasPriceCeiling, + PageDto.IsInfinity, + PageDto.UseCount, + PageDto.IsForInvitation, + PageDto.IsSpecialOffer, + PageDto.IsForFirstPurchase, + PageDto.ProductId, + PageDto.CategoryId); + await _restWrapper.CrudDtoApiRest(Address.DiscountController).Create(request, token); _snackbar.Add($"ساخت تخفیف با موفقیت انجام شد", Severity.Success); _mudDialog.Close(true); } catch (ApiException ex) { - var exe = await ex.GetContentAsAsync(); - _snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error); + if (ex.StatusCode == HttpStatusCode.BadRequest) + { + var exe = await ex.GetContentAsAsync(); + _snackbar.Add(exe != null ? exe.Message : ex.Content, Severity.Error); + } + else + _snackbar.Add(ex.Content, Severity.Error); _mudDialog.Cancel(); } catch (Exception e) @@ -248,7 +274,30 @@ public class DiscountActionDialogBoxViewModel : BaseViewModel if (token == null) throw new Exception("Token is null"); PageDto.Id = _discountId; - var request = PageDto.Adapt(); + var request = new UpdateDiscountCommand( + PageDto.Id, + PageDto.Code, + PageDto.Description, + PageDto.DiscountPercent, + PageDto.DiscountAmount, + PageDto.HasCode, + PageDto.AmountType, + PageDto.Type, + PageDto.Count, + PageDto.StartDate, + PageDto.ExpireDate, + PageDto.Immortal, + PageDto.PriceFloor, + PageDto.HasPriceFloor, + PageDto.PriceCeiling, + PageDto.HasPriceCeiling, + PageDto.IsInfinity, + PageDto.UseCount, + PageDto.IsForInvitation, + PageDto.IsSpecialOffer, + PageDto.IsForFirstPurchase, + PageDto.ProductId, + PageDto.CategoryId); await _restWrapper.CrudApiRest(Address.DiscountController).Update(request, token); _snackbar.Add($"ویرایش تخفیف با موفقیت انجام شد", Severity.Success); _mudDialog.Close(true); diff --git a/Netina.AdminPanel.PWA/Dialogs/FastProductCreateDialogBox.razor.cs b/Netina.AdminPanel.PWA/Dialogs/FastProductCreateDialogBox.razor.cs index 344a7cb..013d9b2 100644 --- a/Netina.AdminPanel.PWA/Dialogs/FastProductCreateDialogBox.razor.cs +++ b/Netina.AdminPanel.PWA/Dialogs/FastProductCreateDialogBox.razor.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Components.Forms; +using MediatR; +using Microsoft.AspNetCore.Components.Forms; using Netina.Domain.Entities.Brands; namespace Netina.AdminPanel.PWA.Dialogs; @@ -141,20 +142,30 @@ public class FastProductCreateDialogBoxViewModel(ISnackbar snackbar, IRestWrappe } product.Cost *= 10; - var command = product.Adapt() with - { - BeDisplayed = true, - BrandId = brand.Id, - CategoryId = SelectedCategory.Id, - Files = files, - Specifications = specifications - }; - var id = await restWrapper.CrudApiRest(Address.ProductController) - .Create(command, token); + var command = new CreateProductCommand(product.PersianName, product.EnglishName, product.Summery, + product.ExpertCheck, + product.Tags, + product.Warranty, + true, + product.Cost, + product.PackingCost, + product.Stock, + product.HasExpressDelivery, + product.MaxOrderCount, + product.IsSpecialOffer, + brand.Id, + SelectedCategory.Id, + null, + specifications, + files, + new Dictionary(), + new Dictionary()); + await restWrapper.CrudApiRest(Address.ProductController).Create(command, token); } catch (ApiException ex) { - snackbar.Add(ex.Message, Severity.Error); + if (ex.StatusCode != HttpStatusCode.OK) + snackbar.Add(ex.Message, Severity.Error); } catch (Exception e) { diff --git a/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor b/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor index daef7a9..86932c1 100644 --- a/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor +++ b/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor @@ -182,6 +182,59 @@ + اطلاعات متا تگ + می توانید متا تگ های سئو برای صفحه مورد نظر را وارد کنید + + + + + + + + + + + + افزودن + + + + + + + + + + + + حذف + + + + + + + + + سوالات متداول می توانید سوالات متداول شهر موردنظر را وارد کنید @@ -215,7 +268,7 @@ Size="@Size.Small" Variant="@Variant.Outlined" Color="@Color.Error" - OnClick="()=>ViewModel.Faqs.Remove(item.Key)" /> + OnClick="() => ViewModel.Faqs.Remove(item.Key)"/> @item.Key diff --git a/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor.cs b/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor.cs index e01d2d4..0ea2c4d 100644 --- a/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor.cs +++ b/Netina.AdminPanel.PWA/Dialogs/ProductActionDialogBox.razor.cs @@ -1,4 +1,6 @@ -namespace Netina.AdminPanel.PWA.Dialogs; +using Netina.Domain.Entities.Seo; + +namespace Netina.AdminPanel.PWA.Dialogs; public class ProductActionDialogBoxViewModel : BaseViewModel { @@ -89,6 +91,7 @@ public class ProductActionDialogBoxViewModel : BaseViewModel PageDto = productLDto; productLDto.Specifications.ForEach(s => Specifications.Add(s)); productLDto.Files.ForEach(f => Files.Add(f)); + productLDto.MetaTags.ForEach(m => MetaTags.Add(m)); SelectedCategory = new ProductCategorySDto { Id = productLDto.CategoryId, Name = productLDto.CategoryName }; SelectedBrand = new BrandSDto { Id = productLDto.BrandId, PersianName = productLDto.BrandName }; PageDto.IsSpecialOffer = productLDto.IsSpecialOffer; @@ -193,7 +196,7 @@ public class ProductActionDialogBoxViewModel : BaseViewModel PageDto.Specifications, PageDto.Files, Faqs, - new Dictionary()); + MetaTags.ToDictionary(x => x.Type, x => x.Value)); await _restWrapper.CrudApiRest(Address.ProductController).Update(request, token); _snackbar.Add($"ویرایش محصول {PageDto.PersianName} با موفقیت انجام شد", Severity.Success); @@ -258,8 +261,8 @@ public class ProductActionDialogBoxViewModel : BaseViewModel PageDto.Specifications, PageDto.Files, Faqs, - new Dictionary()); - await _restWrapper.CrudApiRest(Address.ProductController).Create(request, token); + MetaTags.ToDictionary(x => x.Type, x => x.Value)); + await _restWrapper.CrudDtoApiRest(Address.ProductController).Create(request, token); _snackbar.Add($"ساخت محصول {PageDto.PersianName} با موفقیت انجام شد", Severity.Success); _mudDialog.Close(DialogResult.Ok(true)); @@ -376,6 +379,25 @@ public class ProductActionDialogBoxViewModel : BaseViewModel } } + public readonly ObservableCollection MetaTags = new(); + public string MetaTagType { get; set; } = string.Empty; + public string MetaTagValue { get; set; } = string.Empty; + public void AddMetaTag() + { + try + { + if (MetaTagType.IsNullOrEmpty()) + throw new Exception("لطفا نوع متا مورد نظر را وارد کنید"); + if (MetaTagValue.IsNullOrEmpty()) + throw new Exception("لطفا مقدار متا مورد نظر را وارد کنید"); + + MetaTags.Add(new MetaTagSDto() { Type = MetaTagType, Value = MetaTagValue }); + } + catch (Exception e) + { + _snackbar.Add(e.Message, Severity.Error); + } + } public void AddSpecification() { diff --git a/Netina.AdminPanel.PWA/Dialogs/StorageDialogBox.razor.cs b/Netina.AdminPanel.PWA/Dialogs/StorageDialogBox.razor.cs index d2fd561..262e531 100644 --- a/Netina.AdminPanel.PWA/Dialogs/StorageDialogBox.razor.cs +++ b/Netina.AdminPanel.PWA/Dialogs/StorageDialogBox.razor.cs @@ -115,7 +115,7 @@ public class StorageDialogBoxViewModel : BaseViewModel IsProcessing = true; using var memoryStream = new MemoryStream(); var file = obj.File; - var stream = file.OpenReadStream(); + var stream = file.OpenReadStream(8000000); await stream.CopyToAsync(memoryStream); var fileUpload = new FileUploadRequest diff --git a/Netina.AdminPanel.PWA/Netina.AdminPanel.PWA.csproj b/Netina.AdminPanel.PWA/Netina.AdminPanel.PWA.csproj index af06988..c3fc990 100644 --- a/Netina.AdminPanel.PWA/Netina.AdminPanel.PWA.csproj +++ b/Netina.AdminPanel.PWA/Netina.AdminPanel.PWA.csproj @@ -5,8 +5,8 @@ enable enable service-worker-assets.js - 1.6.18.28 - 1.6.18.28 + 1.7.20.34 + 1.7.20.34 $(MSBuildProjectName) diff --git a/Netina.AdminPanel.PWA/Pages/BrandsPage.razor b/Netina.AdminPanel.PWA/Pages/BrandsPage.razor index a454dd6..333d4a7 100644 --- a/Netina.AdminPanel.PWA/Pages/BrandsPage.razor +++ b/Netina.AdminPanel.PWA/Pages/BrandsPage.razor @@ -1,10 +1,13 @@ @page "/product/brands" +@using StringExtensions = Netina.Common.Extensions.StringExtensions @inject IDialogService DialogService @inject NavigationManager NavigationManager @inject IRestWrapper RestWrapper @inject ISnackbar Snackbar @inject IUserUtility UserUtility +@inject IConfiguration Configuration +@inject IJSRuntime JsRuntime @@ -25,7 +28,7 @@ - @if (@context.Item.HasSpecialPage) @@ -54,15 +56,22 @@ + + + + OnClick="async () => await ViewModel.EditBrandAsync(context.Item)"> @@ -92,8 +101,13 @@ await base.OnInitializedAsync(); } -} - -@code { + private async Task ShowBrand(BrandSDto item) + { + var webUrl = Configuration.GetValue("WebSiteUrl") ?? string.Empty; + var slug = WebUtility.UrlEncode(item.Slug.Replace(' ', '-')); + var url = $"{webUrl}/brands/{item.Id}/{slug}"; + await JsRuntime.InvokeVoidAsync("open", url, "_blank"); + } } + diff --git a/Netina.AdminPanel.PWA/Pages/BrandsPage.razor.cs b/Netina.AdminPanel.PWA/Pages/BrandsPage.razor.cs index 0a72ecd..c460932 100644 --- a/Netina.AdminPanel.PWA/Pages/BrandsPage.razor.cs +++ b/Netina.AdminPanel.PWA/Pages/BrandsPage.razor.cs @@ -23,6 +23,8 @@ public class BrandsPageViewModel( var dto = await restWrapper.CrudDtoApiRest(Address.BrandController) .ReadAll(0); PageDto = dto; + if (PageDto.Count == 10) + PageCount = 2; } catch (ApiException ex) { @@ -108,7 +110,7 @@ public class BrandsPageViewModel( PageDto.Clear(); var dto = await restWrapper.BrandRestApi.ReadAll(CurrentPage, Search); dto.ForEach(d => PageDto.Add(d)); - if (PageDto.Count == 20) + if (PageDto.Count == 10) PageCount = 2; } catch (ApiException ex) @@ -149,7 +151,7 @@ public class BrandsPageViewModel( } dto.ForEach(d => PageDto.Add(d)); - if (PageDto.Count % 20 == 0) + if (PageDto.Count % 10 == 0) PageCount = CurrentPage + 2; } diff --git a/Netina.AdminPanel.PWA/Pages/CategoriesPage.razor.cs b/Netina.AdminPanel.PWA/Pages/CategoriesPage.razor.cs index 4b37565..2f96270 100644 --- a/Netina.AdminPanel.PWA/Pages/CategoriesPage.razor.cs +++ b/Netina.AdminPanel.PWA/Pages/CategoriesPage.razor.cs @@ -152,15 +152,17 @@ public class CategoriesPageViewModel( public bool ChangeOriginalCategoryVisibility() => OriginalCategoryVisibility = !OriginalCategoryVisibility; public void AddFastProductCategory(string categoryName, Guid? parentId = null) { - if(categoryName.IsNullOrEmpty()) + if (categoryName.IsNullOrEmpty()) return; - ProductCategorySDto category = new ProductCategorySDto { Name = categoryName, IsMain = true}; + ProductCategorySDto category = new ProductCategorySDto { Name = categoryName, IsMain = true }; if (parentId != null) { category.IsMain = false; category.ParentId = parentId.Value; + } - var command = category.Adapt() with{Files = new()}; + + var command = new CreateProductCategoryCommand(category.Name, category.Description, category.IsMain, category.ParentId, new(), new Dictionary(), new Dictionary()); Task.Run(async () => { diff --git a/Netina.AdminPanel.PWA/Pages/ProductsPage.razor b/Netina.AdminPanel.PWA/Pages/ProductsPage.razor index 2bc5f92..8b50aef 100644 --- a/Netina.AdminPanel.PWA/Pages/ProductsPage.razor +++ b/Netina.AdminPanel.PWA/Pages/ProductsPage.razor @@ -7,6 +7,8 @@ @inject IUserUtility UserUtility @inject IRestWrapper RestWrapper @inject IBrowserViewportService BrowserViewportService +@inject IConfiguration Configuration +@inject IJSRuntime JsRuntime @@ -180,6 +182,12 @@ + + ("WebSiteUrl") ?? string.Empty; + var slug = WebUtility.UrlEncode(item.Slug.Replace(' ', '-')); + var url = $"{webUrl}/products/{item.Id}/{slug}"; + await JsRuntime.InvokeVoidAsync("open", url, "_blank"); + } } diff --git a/Netina.AdminPanel.PWA/Services/RestServices/ICrudApiRest.cs b/Netina.AdminPanel.PWA/Services/RestServices/ICrudApiRest.cs index bc944c8..0ab3615 100644 --- a/Netina.AdminPanel.PWA/Services/RestServices/ICrudApiRest.cs +++ b/Netina.AdminPanel.PWA/Services/RestServices/ICrudApiRest.cs @@ -24,7 +24,7 @@ public interface ICrudApiRest where T : class public interface ICrudDtoApiRest where T : class where TDto : class { [Post("")] - Task Create([Body] T payload, [Header("Authorization")] string authorization); + Task Create([Body] TCreateCommand payload, [Header("Authorization")] string authorization); [Post("")] Task Create([Body] TDto payload, [Header("Authorization")] string authorization); diff --git a/Netina.AdminPanel.PWA/wwwroot/appsettings.Development.json b/Netina.AdminPanel.PWA/wwwroot/appsettings.Development.json index 937b469..da19de8 100644 --- a/Netina.AdminPanel.PWA/wwwroot/appsettings.Development.json +++ b/Netina.AdminPanel.PWA/wwwroot/appsettings.Development.json @@ -16,9 +16,9 @@ //"WebSiteUrl": "https://bonsaigallery.shop", //"AdminPanelBaseUrl": "https://admin.bonsaigallery.shop", - //"StorageBaseUrl": "https://storage.bonsaigallery.shop", + //"StorageBaseUrl": "https://storage.bonsaigallery.shop/", //"ApiUrl": "https://api.bonsaigallery.shop/api", - "IsShop": true + //"IsShop": true //"WebSiteUrl": "https://hamyanedalat.com", //"AdminPanelBaseUrl": "https://admin.hamyanedalat.com", diff --git a/ServiceDefaults/Extensions.cs b/ServiceDefaults/Extensions.cs new file mode 100644 index 0000000..2a3f4e0 --- /dev/null +++ b/ServiceDefaults/Extensions.cs @@ -0,0 +1,118 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +// Adds common .NET Aspire services: service discovery, resilience, health checks, and OpenTelemetry. +// This project should be referenced by each service project in your solution. +// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults +public static class Extensions +{ + public static IHostApplicationBuilder AddServiceDefaults(this IHostApplicationBuilder builder) + { + builder.ConfigureOpenTelemetry(); + + builder.AddDefaultHealthChecks(); + + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + // Turn on resilience by default + http.AddStandardResilienceHandler(); + + // Turn on service discovery by default + http.AddServiceDiscovery(); + }); + + // Uncomment the following to restrict the allowed schemes for service discovery. + // builder.Services.Configure(options => + // { + // options.AllowedSchemes = ["https"]; + // }); + + return builder; + } + + public static IHostApplicationBuilder ConfigureOpenTelemetry(this IHostApplicationBuilder builder) + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddAspNetCoreInstrumentation() + // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) + //.AddGrpcClientInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static IHostApplicationBuilder AddOpenTelemetryExporters(this IHostApplicationBuilder builder) + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package) + //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"])) + //{ + // builder.Services.AddOpenTelemetry() + // .UseAzureMonitor(); + //} + + return builder; + } + + public static IHostApplicationBuilder AddDefaultHealthChecks(this IHostApplicationBuilder builder) + { + builder.Services.AddHealthChecks() + // Add a default liveness check to ensure app is responsive + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + // Adding health checks endpoints to applications in non-development environments has security implications. + // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments. + if (app.Environment.IsDevelopment()) + { + // All health checks must pass for app to be considered ready to accept traffic after starting + app.MapHealthChecks("/health"); + + // Only health checks tagged with the "live" tag must pass for app to be considered alive + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} diff --git a/ServiceDefaults/ServiceDefaults.csproj b/ServiceDefaults/ServiceDefaults.csproj new file mode 100644 index 0000000..9f4d048 --- /dev/null +++ b/ServiceDefaults/ServiceDefaults.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + + + +