From cf7ba1ef14d33b2491ccf0c0d2d1b442a1601ef1 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Sun, 25 Jun 2023 19:25:13 -0700 Subject: [PATCH 01/25] Pagination added, still raw --- .../Common.Application.csproj | 2 +- .../Common.Infrastructure.ApiClient.csproj | 3 +- .../Core.Application/Core.Application.csproj | 2 +- src/Subjects/Dockerfile | 23 +++++ src/Subjects/Goodtocode.Subjects.sln | 6 ++ .../Infrastructure.Persistence.csproj | 10 +- .../Controllers/BusinessController.cs | 2 +- .../Controllers/BusinessesController.cs | 9 +- .../Goodtocode.Subjects.WebApi.xml | 2 +- .../Presentation.Api.WebApi.csproj | 10 +- .../Presentation.Shared.Rcl/Component1.razor | 3 + .../Component1.razor.css | 6 ++ .../ExampleJsInterop.cs | 36 +++++++ .../Paging/PageHistoryState.cs | 32 ++++++ .../Paging/PagedResultBase.cs | 19 ++++ .../Paging/PagedResultExtension.cs | 20 ++++ .../Paging/PagedResultT.cs | 16 +++ .../Paging/Pager.razor | 50 ++++++++++ .../Paging/Pager.razor.css | 13 +++ .../Presentation.Shared.Rcl.csproj | 20 ++++ .../Presentation.Shared.Rcl/_Imports.razor | 1 + .../wwwroot/background.png | Bin 0 -> 378 bytes .../wwwroot/exampleJsInterop.js | 6 ++ .../Models/BusinessCreateModel.cs | 11 --- .../Models/BusinessModel.cs | 16 +++ .../Models/BusinessSearchModel.cs | 9 -- .../Models/BusinessTypes.cs | 15 +++ .../Models/BusinessUpdateModel.cs | 13 --- .../Models/BusinessValidator.cs | 16 +++ .../Pages/Business/BusinessCreate.razor | 2 +- .../Pages/Business/BusinessForm.razor | 78 +++++++++++++++ .../Pages/Business/BusinessList.razor | 92 ++++++++++++++++-- .../Presentation.Web.BlazorServer.csproj | 21 ++-- .../Presentation.Web.BlazorServer/Program.cs | 7 +- .../{Data => Services}/BusinessService.cs | 47 +++------ .../Services/IBusinessService.cs | 21 ++++ .../Presentation.Web.BlazorStatic.csproj | 6 +- src/Subjects/docker-compose.yml | 7 ++ 38 files changed, 531 insertions(+), 121 deletions(-) create mode 100644 src/Subjects/Dockerfile create mode 100644 src/Subjects/Presentation.Shared.Rcl/Component1.razor create mode 100644 src/Subjects/Presentation.Shared.Rcl/Component1.razor.css create mode 100644 src/Subjects/Presentation.Shared.Rcl/ExampleJsInterop.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/PageHistoryState.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor create mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor.css create mode 100644 src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj create mode 100644 src/Subjects/Presentation.Shared.Rcl/_Imports.razor create mode 100644 src/Subjects/Presentation.Shared.Rcl/wwwroot/background.png create mode 100644 src/Subjects/Presentation.Shared.Rcl/wwwroot/exampleJsInterop.js delete mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/BusinessCreateModel.cs create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs delete mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/BusinessSearchModel.cs create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/BusinessTypes.cs delete mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/BusinessUpdateModel.cs create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/BusinessValidator.cs create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor rename src/Subjects/Presentation.Web.BlazorServer/{Data => Services}/BusinessService.cs (54%) create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs create mode 100644 src/Subjects/docker-compose.yml diff --git a/src/Subjects/Common/Common.Application/Common.Application.csproj b/src/Subjects/Common/Common.Application/Common.Application.csproj index 6bdaec48..f66dc77a 100644 --- a/src/Subjects/Common/Common.Application/Common.Application.csproj +++ b/src/Subjects/Common/Common.Application/Common.Application.csproj @@ -13,6 +13,6 @@ - + diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj b/src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj index abe556ec..f7b8421b 100644 --- a/src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj +++ b/src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj @@ -10,11 +10,10 @@ - - + diff --git a/src/Subjects/Core.Application/Core.Application.csproj b/src/Subjects/Core.Application/Core.Application.csproj index 92c7380e..1fe699c8 100644 --- a/src/Subjects/Core.Application/Core.Application.csproj +++ b/src/Subjects/Core.Application/Core.Application.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Subjects/Dockerfile b/src/Subjects/Dockerfile new file mode 100644 index 00000000..7676027b --- /dev/null +++ b/src/Subjects/Dockerfile @@ -0,0 +1,23 @@ +### PREPARE +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env +WORKDIR /src + +### Copy csproj and sln and restore as distinct layers +COPY Blazorcrud.Client/*.csproj Blazorcrud.Client/ +COPY Blazorcrud.Server/*.csproj Blazorcrud.Server/ +COPY Blazorcrud.Shared/*.csproj Blazorcrud.Shared/ +COPY Blazorcrud.sln . +RUN dotnet restore + +### PUBLISH +FROM build-env as publish-env +COPY . . +RUN dotnet publish "Blazorcrud.sln" -c Release -o /app + +### RUNTIME IMAGE +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime-env +WORKDIR /app +COPY --from=publish-env /app . +ENV ASPNETCORE_URLS=http://+:80 +EXPOSE 80 +ENTRYPOINT ["dotnet", "Blazorcrud.Server.dll"] \ No newline at end of file diff --git a/src/Subjects/Goodtocode.Subjects.sln b/src/Subjects/Goodtocode.Subjects.sln index 4142f06b..de760a01 100644 --- a/src/Subjects/Goodtocode.Subjects.sln +++ b/src/Subjects/Goodtocode.Subjects.sln @@ -27,6 +27,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Specs.Integration", "Specs. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Specs.Unit", "Specs.Unit\Specs.Unit.csproj", "{B1912655-E309-4886-A882-1C384ACFB165}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation.Shared.Rcl", "Presentation.Shared.Rcl\Presentation.Shared.Rcl.csproj", "{52F8BCD2-BB2D-4A23-8638-BBDC777D167B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -77,6 +79,10 @@ Global {B1912655-E309-4886-A882-1C384ACFB165}.Debug|Any CPU.Build.0 = Debug|Any CPU {B1912655-E309-4886-A882-1C384ACFB165}.Release|Any CPU.ActiveCfg = Release|Any CPU {B1912655-E309-4886-A882-1C384ACFB165}.Release|Any CPU.Build.0 = Release|Any CPU + {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Subjects/Infrastructure.Persistence/Infrastructure.Persistence.csproj b/src/Subjects/Infrastructure.Persistence/Infrastructure.Persistence.csproj index 7d1228a7..1a4ef347 100644 --- a/src/Subjects/Infrastructure.Persistence/Infrastructure.Persistence.csproj +++ b/src/Subjects/Infrastructure.Persistence/Infrastructure.Persistence.csproj @@ -8,12 +8,12 @@ enable - - - - + + + + - + diff --git a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs index 54f88e13..784c84b9 100644 --- a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs +++ b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs @@ -74,7 +74,7 @@ public async Task Put([FromBody] BusinessObject business) var command = business.CopyPropertiesSafe(); var createdEntity = await Mediator.Send(command); - return Created(new Uri($"{Request.Path}/{createdEntity.Key}", UriKind.Relative), createdEntity); + return Created(new Uri($"{Request.Path}/{createdEntity.BusinessKey}", UriKind.Relative), createdEntity); } /// diff --git a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs index 6c4ef3f1..9c210bbe 100644 --- a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs +++ b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs @@ -24,11 +24,8 @@ public class BusinessesController : BaseController [HttpGet(Name = "GetBusinessesByNameQuery")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Get(string name) + public async Task> Get(string name, int pageNumber = 1, int pageSize = 20) => await Mediator.Send(new GetBusinessesByNameQuery { - return await Mediator.Send(new GetBusinessesByNameQuery - { - BusinessName = name - }); - } + BusinessName = name + }); } \ No newline at end of file diff --git a/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml b/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml index 9b27486d..6500b32a 100644 --- a/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml +++ b/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml @@ -120,7 +120,7 @@ Businesses Controller V1.0 - + Get Businesses by Name Sample request: diff --git a/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj b/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj index c88d2c67..15d862d0 100644 --- a/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj +++ b/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj @@ -17,14 +17,14 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + diff --git a/src/Subjects/Presentation.Shared.Rcl/Component1.razor b/src/Subjects/Presentation.Shared.Rcl/Component1.razor new file mode 100644 index 00000000..8fe97540 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Component1.razor @@ -0,0 +1,3 @@ +
+ This component is defined in the Goodtocode.Subjects.Rcl library. +
diff --git a/src/Subjects/Presentation.Shared.Rcl/Component1.razor.css b/src/Subjects/Presentation.Shared.Rcl/Component1.razor.css new file mode 100644 index 00000000..c6afca40 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Component1.razor.css @@ -0,0 +1,6 @@ +.my-component { + border: 2px dashed red; + padding: 1em; + margin: 1em 0; + background-image: url('background.png'); +} diff --git a/src/Subjects/Presentation.Shared.Rcl/ExampleJsInterop.cs b/src/Subjects/Presentation.Shared.Rcl/ExampleJsInterop.cs new file mode 100644 index 00000000..c825d95c --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/ExampleJsInterop.cs @@ -0,0 +1,36 @@ +using Microsoft.JSInterop; + +namespace Goodtocode.Subjects.Rcl; + +// This class provides an example of how JavaScript functionality can be wrapped +// in a .NET class for easy consumption. The associated JavaScript module is +// loaded on demand when first needed. +// +// This class can be registered as scoped DI service and then injected into Blazor +// components for use. + +public class ExampleJsInterop : IAsyncDisposable +{ + private readonly Lazy> moduleTask; + + public ExampleJsInterop(IJSRuntime jsRuntime) + { + moduleTask = new(() => jsRuntime.InvokeAsync( + "import", "./_content/Presentation.Shared.Rcl/exampleJsInterop.js").AsTask()); + } + + public async ValueTask Prompt(string message) + { + var module = await moduleTask.Value; + return await module.InvokeAsync("showPrompt", message); + } + + public async ValueTask DisposeAsync() + { + if (moduleTask.IsValueCreated) + { + var module = await moduleTask.Value; + await module.DisposeAsync(); + } + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PageHistoryState.cs b/src/Subjects/Presentation.Shared.Rcl/Paging/PageHistoryState.cs new file mode 100644 index 00000000..59d61b5f --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/PageHistoryState.cs @@ -0,0 +1,32 @@ +namespace Goodtocode.Subjects.Rcl; + +public class PageHistoryState +{ + private List previousPages; + + public PageHistoryState() + { + previousPages = new List(); + } + + public void AddPageToHistory(string PageName) + { + previousPages.Add(PageName); + } + + public string GetGoBackPage() + { + if (previousPages.Count > 1) + { + // page added on initialization, return second from last + return previousPages.ElementAt(previousPages.Count - 1); + } + // can't go back page + return previousPages.FirstOrDefault(); + } + + public bool CanGoBack() + { + return previousPages.Count > 1; + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs new file mode 100644 index 00000000..f12a755a --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs @@ -0,0 +1,19 @@ +namespace Goodtocode.Subjects.Rcl; + +public abstract class PagedResultBase +{ + public int CurrentPage { get; set; } + public int PageCount { get; set; } + public int PageSize { get; set; } + public int RowCount { get; set; } + + public int FirstRowOnPage + { + get { return (CurrentPage - 1) * PageSize + 1; } + } + + public int LastRowOnPage + { + get { return Math.Min(CurrentPage * PageSize, RowCount); } + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs new file mode 100644 index 00000000..5a9fbd76 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs @@ -0,0 +1,20 @@ +namespace Goodtocode.Subjects.Rcl; + +public static class PagedResultExtensions +{ + public static PagedResult GetPaged(this IQueryable query, int page, int pageSize) where T : class + { + var result = new PagedResult(); + result.CurrentPage = page; + result.PageSize = pageSize; + result.RowCount = query.Count(); + + var pageCount = (double)result.RowCount / pageSize; + result.PageCount = (int)Math.Ceiling(pageCount); + + var skip = (page - 1) * pageSize; + result.Results = query.Skip(skip).Take(pageSize).ToList(); + + return result; + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs new file mode 100644 index 00000000..f985adb1 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs @@ -0,0 +1,16 @@ +namespace Goodtocode.Subjects.Rcl; + +public class PagedResult : PagedResultBase where T : class +{ + public IList Results { get; set; } + + public PagedResult() + { + Results = new List(); + } + + public PagedResult(List list) + { + Results = list; + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor new file mode 100644 index 00000000..75d5c791 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor @@ -0,0 +1,50 @@ +@if (Result != null) +{ +
+
+ @if (Result.PageCount > 1) + { +
    +
  • + @for (var i = StartIndex; i <= FinishIndex; i++) + { + var currentIndex = i; + @if (i == Result.CurrentPage) + { +
  • @i
  • + } + else + { +
  • + } + } +
  • +
  • Page @Result.CurrentPage of @Result.PageCount
  • +
+ } +
+
+} + +@code { + [Parameter] + public PagedResultBase Result { get; set; } + + [Parameter] + public Action PageChanged { get; set; } + + protected int StartIndex { get; private set; } = 0; + protected int FinishIndex { get; private set; } = 0; + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + StartIndex = Math.Max(Result.CurrentPage - 5, 1); + FinishIndex = Math.Min(Result.CurrentPage + 5, Result.PageCount); + } + + protected void PagerButtonClicked(int page) + { + PageChanged?.Invoke(page); + } +} diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor.css b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor.css new file mode 100644 index 00000000..497c3ae1 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor.css @@ -0,0 +1,13 @@ +/* Pagination Style */ +.pagination .btn { + background-color: darkgray; +} + +.pagination span.btn { + background-color: #000; + color: #fff; +} + +.pagination li { + padding: 5px; +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj b/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj new file mode 100644 index 00000000..e91e9c17 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj @@ -0,0 +1,20 @@ + + + + Goodtocode.Subjects.Rcl + Goodtocode.Subjects.Rcl + 1.0.0 + net7.0 + enable + enable + + + + + + + + + + + diff --git a/src/Subjects/Presentation.Shared.Rcl/_Imports.razor b/src/Subjects/Presentation.Shared.Rcl/_Imports.razor new file mode 100644 index 00000000..77285129 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/src/Subjects/Presentation.Shared.Rcl/wwwroot/background.png b/src/Subjects/Presentation.Shared.Rcl/wwwroot/background.png new file mode 100644 index 0000000000000000000000000000000000000000..e15a3bde6e2bdb380df6a0b46d7ed00bdeb0aaa8 GIT binary patch literal 378 zcmeAS@N?(olHy`uVBq!ia0vp^x**KK1SGdsl%54rjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucLCF%=h?3y^w370~qEv>0#LT=By}Z;C1rt33 zJwr2>%=KS^ie7oTIEF;HpS|GCbyPusHSqiXaCu3qf)82(9Gq&mZq2{Kq}M*X&MWtJ zSi1Jo7ZzfImg%g=t(qo=wsSR2lZoP(Rj#3wacN=q0?Br(rXzgZEGK2$ID{|A=5S{xJEuzSH>!M+7wSY6hB<=-E^*n0W7 S8wY^CX7F_Nb6Mw<&;$S{dxtsz literal 0 HcmV?d00001 diff --git a/src/Subjects/Presentation.Shared.Rcl/wwwroot/exampleJsInterop.js b/src/Subjects/Presentation.Shared.Rcl/wwwroot/exampleJsInterop.js new file mode 100644 index 00000000..ea8d76ad --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/wwwroot/exampleJsInterop.js @@ -0,0 +1,6 @@ +// This is a JavaScript module that is loaded on demand. It can export any number of +// functions, and may import other JavaScript modules if required. + +export function showPrompt(message) { + return prompt(message, 'Type anything here'); +} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessCreateModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessCreateModel.cs deleted file mode 100644 index 67464e9d..00000000 --- a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessCreateModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Goodtocode.Subjects.Domain; -using System.ComponentModel.DataAnnotations; - -namespace Goodtocode.Subjects.BlazorServer.Models; - -public class BusinessCreateModel : BusinessObject -{ - [Required] - public string BusinessName { get; set; } = string.Empty; - public string TaxNumber { get; set; } = string.Empty; -} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs new file mode 100644 index 00000000..4a6c2d45 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs @@ -0,0 +1,16 @@ +using Goodtocode.Subjects.Domain; +using System.ComponentModel.DataAnnotations; + +namespace Goodtocode.Subjects.BlazorServer.Models; + +public class BusinessModel : IBusinessEntity +{ + [Required] + public string Name { get; set; } = string.Empty; + public Guid BusinessKey { get; set; } = default; + public string BusinessName { get; set; } = string.Empty; + public string TaxNumber { get; set; } = string.Empty; + public string Type { get; set; } = string.Empty; + public string PhoneNumber { get; set; } = string.Empty; + public bool IsDeleting { get; set; } +} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessSearchModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessSearchModel.cs deleted file mode 100644 index 26efd455..00000000 --- a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessSearchModel.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Goodtocode.Subjects.BlazorServer.Models; - -public class BusinessSearchModel -{ - [Required] - public string Name { get; set; } = string.Empty; -} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessTypes.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessTypes.cs new file mode 100644 index 00000000..3c1d3bc0 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessTypes.cs @@ -0,0 +1,15 @@ +namespace Blazorcrud.Shared.Models +{ + public enum BusinessTypes + { + SP, + Partnership, + Corporation, + MNC, + NPO, + Franchise, + LLC, + Trust, + Other + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessUpdateModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessUpdateModel.cs deleted file mode 100644 index b0f8a193..00000000 --- a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessUpdateModel.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Goodtocode.Subjects.Domain; -using System.ComponentModel.DataAnnotations; - -namespace Goodtocode.Subjects.BlazorServer.Models; - -public class BusinessUpdateModel : BusinessEntity -{ - [Required] - public Guid BusinessKey { get; set; } = default; - [Required] - public string BusinessName { get; set; } = string.Empty; - public string TaxNumber { get; set; } = string.Empty; -} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessValidator.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessValidator.cs new file mode 100644 index 00000000..512a0fae --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; + +namespace Goodtocode.Subjects.BlazorServer.Models; + +public class BusinessValidator : AbstractValidator +{ + public BusinessValidator() + { + ValidatorOptions.Global.DefaultRuleLevelCascadeMode = CascadeMode.Stop; + + RuleFor(person => person.BusinessName).NotEmpty().WithMessage("Business name is a required field.") + .Length(3, 50).WithMessage("Business name must be between 3 and 50 characters."); + //RuleFor(person => person.Addresses).NotEmpty().WithMessage("You have to define at least one address per person"); + //RuleForEach(person => person.Addresses).SetValidator(new AddressValidator()); + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor index 85d06c63..f33db7c2 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor @@ -29,7 +29,7 @@ @code { - private BusinessCreateModel business = new BusinessCreateModel(); + private BusinessModel business = new BusinessModel(); private string alertMessage = string.Empty; private CancellationTokenSource cts = new CancellationTokenSource(); private async Task CreateBusineses() diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor new file mode 100644 index 00000000..7844e35f --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor @@ -0,0 +1,78 @@ +@using Goodtocode.Subjects.BlazorServer.Models; +@inject Goodtocode.Subjects.Rcl.PageHistoryState PageHistoryState + + + +
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+ + + + + + + + + + + + + +
+
+
+
+
+ +
+ + +
+
+
+
+ +
+ + @if (PageHistoryState.CanGoBack()){ + Cancel + } + else{ + Back + } +
+ +
+ +@code { + [Parameter] + public BusinessModel business { get; set; } + [Parameter] + public string ButtonText { get; set; } = "Save"; + [Parameter] + public bool loading {get; set;} = false; + [Parameter] + public EventCallback OnValidSubmit { get; set; } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index 01596e3d..5a758bc5 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -3,16 +3,20 @@ @using Goodtocode.Subjects.BlazorServer.Models; @using Goodtocode.Subjects.Domain; @using System.ComponentModel.DataAnnotations; +@using Goodtocode.Subjects.Rcl; +@using Microsoft.AspNetCore.Http.Extensions; @inject BusinessService Service +@inject PageHistoryState PageHistory +@inject NavigationManager UriHelper Business Search - +
+ @bind="@SearchTerm">
@@ -23,7 +27,7 @@
-@if (businesses.Count() > 0) +@if (businesses.Results.Count() > 0) { @@ -31,15 +35,34 @@ + + - @foreach (var business in businesses) + @foreach (var business in businesses.Results) { + } @@ -47,13 +70,48 @@ } @code { + [Parameter] + public int Page { get; set; } = 1; + [Parameter] + public string SearchTerm { get; set; } = string.Empty; private string alertMessage = string.Empty; - private BusinessSearchModel businessSearch = new BusinessSearchModel(); - private IEnumerable businesses = new List(); + private BusinessModel businessSearch = new BusinessModel(); + private PagedResult businesses = new PagedResult(); private CancellationTokenSource cts = new CancellationTokenSource(); private bool processing; - private async Task GetBusineses() + protected override void OnInitialized() + { + PageHistory.AddPageToHistory(UriHelper.Uri); + base.OnInitialized(); + } + + protected override async Task OnParametersSetAsync() + { + businesses = await Service.GetBusinessesAsync(null, Page); + PageHistory.AddPageToHistory(UriHelper.Uri); + } + + protected async Task SearchBoxKeyPress(KeyboardEventArgs ev) + { + if (ev.Key == "Enter") + { + await SearchClick(); + } + } + + protected async Task SearchClick() + { + if (string.IsNullOrEmpty(SearchTerm)) + { + businesses = await Service.GetBusinessesAsync(null, Page); + return; + } + businesses = await Service.GetBusinessesAsync(SearchTerm, Page); + StateHasChanged(); + } + + private async Task GetBusinesses() { alertMessage = string.Empty; @@ -65,9 +123,9 @@ try { processing = true; - await Task.Delay(500, cts.Token); - businesses = await Service.GetBusinessesAsync(businessSearch.Name); - if (businesses.Count() == 0) + await Task.Delay(500, cts.Token); + businesses = await Service.GetBusinessesAsync(businessSearch.Name, 1); + if (businesses.Results.Count() == 0) alertMessage = "No businesses found"; } catch (TaskCanceledException) @@ -79,4 +137,18 @@ processing = false; } } + + private async Task DeleteBusiness(Guid businessKey) + { + + } + + //private async void DeletePerson(Person _person) + //{ + // var person = _person; + // person.IsDeleting = true; + // await PersonService.DeletePerson(person.PersonId); + // people = await PersonService.GetPeople(null, Page); + // StateHasChanged(); + //} } diff --git a/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj b/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj index bc28100d..7f8a7c88 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj +++ b/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj @@ -11,34 +11,25 @@ 0 - - - - - + - - + + - - + + - - - - - true - + diff --git a/src/Subjects/Presentation.Web.BlazorServer/Program.cs b/src/Subjects/Presentation.Web.BlazorServer/Program.cs index acf7715e..af0f9ae7 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Program.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Program.cs @@ -1,6 +1,7 @@ using Azure.Identity; using Goodtocode.Common.Infrastructure.ApiClient; using Goodtocode.Subjects.BlazorServer.Data; +using Goodtocode.Subjects.Rcl; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Identity.Web; using Microsoft.Identity.Web.UI; @@ -32,10 +33,9 @@ builder.Services.AddServerSideBlazor() .AddMicrosoftIdentityConsentHandler(); + if (builder.Environment.IsDevelopment() || string.Equals(builder.Environment.EnvironmentName, "local", StringComparison.InvariantCultureIgnoreCase)) -{ - builder.WebHost.UseStaticWebAssets(); -} + builder.WebHost.UseStaticWebAssets(); // Required to serve static files from the wwwroot folder in environments other than Development. builder.Services.AddApplicationInsightsTelemetry(options => { @@ -51,6 +51,7 @@ ); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/src/Subjects/Presentation.Web.BlazorServer/Data/BusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs similarity index 54% rename from src/Subjects/Presentation.Web.BlazorServer/Data/BusinessService.cs rename to src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs index 3d920d04..adae03de 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Data/BusinessService.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs @@ -1,15 +1,13 @@ using Goodtocode.Common.Extensions; -using Goodtocode.Subjects.Application; using Goodtocode.Subjects.BlazorServer.Models; -using Goodtocode.Subjects.BlazorServer.Pages.Business; using Goodtocode.Subjects.Domain; -using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Goodtocode.Subjects.Rcl; using System.Net; using System.Text.Json; namespace Goodtocode.Subjects.BlazorServer.Data; -public class BusinessService +public class BusinessService : IBusinessService { private readonly IHttpClientFactory _clientFactory; @@ -18,73 +16,54 @@ public BusinessService(IHttpClientFactory clientFactory) _clientFactory = clientFactory; } - public async Task GetBusinessAsync(Guid businessKey) + public async Task GetBusinessAsync(Guid businessKey) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); var response = await httpClient.GetAsync($"{httpClient.BaseAddress}/Business?key={businessKey}&api-version=1"); - var business = new BusinessEntity(); + var business = new BusinessModel(); if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); - business = JsonSerializer.Deserialize(response.Content.ReadAsStream()); + business = JsonSerializer.Deserialize(response.Content.ReadAsStream()); if (business == null) throw new Exception(); } return business; } - public async Task> GetBusinessesAsync(string name) + public async Task> GetBusinessesAsync(string name, int page) { - var business = new List(); + var business = new PagedResult(); var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); var response = await httpClient.GetAsync($"{httpClient.BaseAddress}/Businesses?name={name}&api-version=1"); if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); - business = JsonSerializer.Deserialize>(response.Content.ReadAsStream()) ?? throw new Exception("Deserialization failed."); + business = new PagedResult(JsonSerializer.Deserialize>(response.Content.ReadAsStream()) + ?? throw new Exception("Deserialization failed.")); } return business; } - public async Task CreateBusinessAsync(BusinessObject business) + public async Task CreateBusinessAsync(BusinessModel business) { - BusinessEntity? businessCreated = new(); var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.PutAsJsonAsync($"{httpClient.BaseAddress}/Business?api-version=1", business); - - if (response.StatusCode == HttpStatusCode.Created) - businessCreated = JsonSerializer.Deserialize(await response.Content.ReadAsStreamAsync()) ?? throw new Exception("Deserialization failed."); - + var response = await httpClient.PutAsJsonAsync($"{httpClient.BaseAddress}/Business?api-version=1", business.CopyPropertiesSafe()); response.EnsureSuccessStatusCode(); - - return businessCreated; } - public async Task UpdateBusinessAsync(BusinessUpdateModel business) + public async Task UpdateBusinessAsync(BusinessModel business) { - BusinessEntity? businessUpdated = null; var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); var response = await httpClient.PostAsJsonAsync($"{httpClient.BaseAddress}/Business?key={business.BusinessKey}api-version=1", business.CopyPropertiesSafe()); - - if (response.StatusCode == HttpStatusCode.OK) - businessUpdated = JsonSerializer.Deserialize(await response.Content.ReadAsStreamAsync()); - if (businessUpdated == null) - throw new Exception(); - response.EnsureSuccessStatusCode(); - - return businessUpdated; } - public async Task DeleteBusinessAsync(Guid businessKey) + public async Task DeleteBusinessAsync(Guid businessKey) { - BusinessEntity? businessUpdated = null; var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); var response = await httpClient.DeleteAsync($"{httpClient.BaseAddress}/Business?key={businessKey}api-version=1"); - response.EnsureSuccessStatusCode(); - - return businessUpdated; } } \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs new file mode 100644 index 00000000..0ce84736 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs @@ -0,0 +1,21 @@ +using Goodtocode.Subjects.BlazorServer.Models; +using Goodtocode.Subjects.Domain; +using Goodtocode.Subjects.Rcl; +using System; + +namespace Goodtocode.Subjects.BlazorServer.Data +{ + public interface IBusinessService + { + Task> GetBusinessesAsync(string name, int page); + + Task GetBusinessAsync(Guid businessKey); + + Task CreateBusinessAsync(BusinessModel business); + + Task UpdateBusinessAsync(BusinessModel business); + + Task DeleteBusinessAsync(Guid businessKey); + + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorStatic/Presentation.Web.BlazorStatic.csproj b/src/Subjects/Presentation.Web.BlazorStatic/Presentation.Web.BlazorStatic.csproj index f3b97427..78d60107 100644 --- a/src/Subjects/Presentation.Web.BlazorStatic/Presentation.Web.BlazorStatic.csproj +++ b/src/Subjects/Presentation.Web.BlazorStatic/Presentation.Web.BlazorStatic.csproj @@ -10,9 +10,9 @@ - - - + + + diff --git a/src/Subjects/docker-compose.yml b/src/Subjects/docker-compose.yml new file mode 100644 index 00000000..e957e081 --- /dev/null +++ b/src/Subjects/docker-compose.yml @@ -0,0 +1,7 @@ +version: "3.9" +services: + blazorcrud: + image: thbst16/blazor-crud + build: . + ports: + - 8080:80 \ No newline at end of file From 66a78893d18aedd884e0528337f0633eb11015b6 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Tue, 27 Jun 2023 14:11:45 -0700 Subject: [PATCH 02/25] updated build/debug json --- .vscode/launch.json | 4 +- .vscode/tasks.json | 6 +- .../Goodtocode.Subjects.WebApi.xml | 266 +++++++++--------- 3 files changed, 138 insertions(+), 138 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index c982fbd8..2d8f6dd3 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,9 +10,9 @@ "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/src/Subjects/Presentation/WebApi/bin/Debug/net7.0/Goodtocode.Subjects.WebApi.dll", + "program": "${workspaceFolder}/src/Subjects/Presentation.Api.WebApi/bin/Debug/net7.0/Goodtocode.Subjects.WebApi.dll", "args": [], - "cwd": "${workspaceFolder}/src/Subjects/Presentation/WebApi", + "cwd": "${workspaceFolder}/src/Subjects/Presentation.Api.WebApi", "stopAtEntry": false, // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser "serverReadyAction": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bcb84a53..e9ad4138 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -7,7 +7,7 @@ "type": "process", "args": [ "build", - "${workspaceFolder}/src/Subjects/Presentation/WebApi/WebApi.csproj", + "${workspaceFolder}/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -23,7 +23,7 @@ "type": "process", "args": [ "publish", - "${workspaceFolder}/src/Subjects/Presentation/WebApi/WebApi.csproj", + "${workspaceFolder}/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj", "/property:GenerateFullPaths=true", "/consoleloggerparameters:NoSummary" ], @@ -37,7 +37,7 @@ "watch", "run", "--project", - "${workspaceFolder}/src/Subjects/Presentation/WebApi/WebApi.csproj" + "${workspaceFolder}/src/Subjects/Presentation.Api.WebApi/Presentation.Api.WebApi.csproj" ], "problemMatcher": "$msCompile" } diff --git a/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml b/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml index 6500b32a..a578067f 100644 --- a/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml +++ b/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml @@ -1,133 +1,133 @@ - - - - Goodtocode.Subjects.WebApi - - - - - Filter to handle ApiExceptionFilterAttribute - - - - - ApiExceptionFilterAttribute including ValidationException, NotFoundException, UnauthorizedAccessException, - ForbiddenAccessException - NotFoundException - - - - - Handles OnException - - - - - - Base Controller - - - - - Defines a mediator to encapsulate request/response and publishing interaction - - - - - Configures swagger options - - - - - Constructor - - - - - - Configures the swagger options by delegate - - - - - - Configures options - - - - - - - Businesses Controller V1.0 - - - - Get Business by Key - - Sample request: - "businessKey": "d3d42e6e-87c5-49d6-aec0-7995711d6612" - "api-version": 1 - - BusinessEntity - - - - Add Business - - - Sample request: - "api-version": 1 - HttpPut Body - { - "BusinessName": "My Business", - "TaxNumber": "12-445666" - } - - Created Item URI and Object - - - - Update Business - - - Sample request: - "BusinessKey": d3d42e6e-87c5-49d6-aec0-7995711d6612, - "api-version": 1 - HttpPost Body - { - "BusinessName": "My Business", - "TaxNumber": "12-445666" - } - - bool - - - - Delete a Business - - - Sample request: - "BusinessKey": d3d42e6e-87c5-49d6-aec0-7995711d6612, - "api-version": 1 - HttpDelete Body - { - } - - bool - - - - Businesses Controller V1.0 - - - - Get Businesses by Name - - Sample request: - "businessName": "My Business" - "api-version": 1 - - Collection of BusinessEntity - - - + + + + Goodtocode.Subjects.WebApi + + + + + Filter to handle ApiExceptionFilterAttribute + + + + + ApiExceptionFilterAttribute including ValidationException, NotFoundException, UnauthorizedAccessException, + ForbiddenAccessException + NotFoundException + + + + + Handles OnException + + + + + + Base Controller + + + + + Defines a mediator to encapsulate request/response and publishing interaction + + + + + Configures swagger options + + + + + Constructor + + + + + + Configures the swagger options by delegate + + + + + + Configures options + + + + + + + Businesses Controller V1.0 + + + + Get Business by Key + + Sample request: + "businessKey": "d3d42e6e-87c5-49d6-aec0-7995711d6612" + "api-version": 1 + + BusinessEntity + + + + Add Business + + + Sample request: + "api-version": 1 + HttpPut Body + { + "BusinessName": "My Business", + "TaxNumber": "12-445666" + } + + Created Item URI and Object + + + + Update Business + + + Sample request: + "BusinessKey": d3d42e6e-87c5-49d6-aec0-7995711d6612, + "api-version": 1 + HttpPost Body + { + "BusinessName": "My Business", + "TaxNumber": "12-445666" + } + + bool + + + + Delete a Business + + + Sample request: + "BusinessKey": d3d42e6e-87c5-49d6-aec0-7995711d6612, + "api-version": 1 + HttpDelete Body + { + } + + bool + + + + Businesses Controller V1.0 + + + + Get Businesses by Name + + Sample request: + "businessName": "My Business" + "api-version": 1 + + Collection of BusinessEntity + + + From a6134b15cf57e6a7cd3a86931b776ddbe3f917dc Mon Sep 17 00:00:00 2001 From: Robert Good Date: Wed, 28 Jun 2023 21:18:29 -0700 Subject: [PATCH 03/25] pagination added to Repo and API --- .../Common.Extensions.csproj | 11 ++ .../PagedResult}/PagedResultBase.cs | 2 +- .../PagedResult}/PagedResultT.cs | 2 +- .../System/ObjectExtensions.cs | 156 +++++++++++++++++ .../System/TypeExtensions.cs | 50 ++++++ .../Common/Common.Domain/Common.Domain.csproj | 4 + .../Extensions/ObjectExtensions.cs | 157 ------------------ .../Extensions/TypeExtensions.cs | 51 ------ .../Queries/GetBusinessesByNameQuery.cs | 12 +- .../Interfaces/IBusinessRepo.cs | 3 +- src/Subjects/Goodtocode.Subjects.sln | 9 +- .../Common/PagedResultExtensions.cs | 22 +++ .../Repositories/BusinessRepo.cs | 12 +- .../Controllers/BusinessesController.cs | 5 +- .../Paging/PagedResultExtension.cs | 20 --- .../Paging/Pager.razor | 1 + .../Presentation.Shared.Rcl.csproj | 4 + .../Pages/Business/BusinessList.razor | 1 + .../Services/IBusinessService.cs | 6 +- .../GetBusinessesByNameStepDefinitions.cs | 7 +- .../GetBusinessesByNameStepDefinitions.cs | 19 ++- 21 files changed, 297 insertions(+), 257 deletions(-) create mode 100644 src/Subjects/Common.Extensions/Common.Extensions.csproj rename src/Subjects/{Presentation.Shared.Rcl/Paging => Common.Extensions/PagedResult}/PagedResultBase.cs (91%) rename src/Subjects/{Presentation.Shared.Rcl/Paging => Common.Extensions/PagedResult}/PagedResultT.cs (86%) create mode 100644 src/Subjects/Common.Extensions/System/ObjectExtensions.cs create mode 100644 src/Subjects/Common.Extensions/System/TypeExtensions.cs delete mode 100644 src/Subjects/Common/Common.Domain/Extensions/ObjectExtensions.cs delete mode 100644 src/Subjects/Common/Common.Domain/Extensions/TypeExtensions.cs create mode 100644 src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs delete mode 100644 src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs diff --git a/src/Subjects/Common.Extensions/Common.Extensions.csproj b/src/Subjects/Common.Extensions/Common.Extensions.csproj new file mode 100644 index 00000000..8919dd76 --- /dev/null +++ b/src/Subjects/Common.Extensions/Common.Extensions.csproj @@ -0,0 +1,11 @@ + + + + Goodtocode.Common.Extensions + Goodtocode.Common.Extensions + net7.0 + enable + enable + + + diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs b/src/Subjects/Common.Extensions/PagedResult/PagedResultBase.cs similarity index 91% rename from src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs rename to src/Subjects/Common.Extensions/PagedResult/PagedResultBase.cs index f12a755a..d74a597f 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultBase.cs +++ b/src/Subjects/Common.Extensions/PagedResult/PagedResultBase.cs @@ -1,4 +1,4 @@ -namespace Goodtocode.Subjects.Rcl; +namespace Goodtocode.Common.Extensions; public abstract class PagedResultBase { diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs b/src/Subjects/Common.Extensions/PagedResult/PagedResultT.cs similarity index 86% rename from src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs rename to src/Subjects/Common.Extensions/PagedResult/PagedResultT.cs index f985adb1..936e8a74 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultT.cs +++ b/src/Subjects/Common.Extensions/PagedResult/PagedResultT.cs @@ -1,4 +1,4 @@ -namespace Goodtocode.Subjects.Rcl; +namespace Goodtocode.Common.Extensions; public class PagedResult : PagedResultBase where T : class { diff --git a/src/Subjects/Common.Extensions/System/ObjectExtensions.cs b/src/Subjects/Common.Extensions/System/ObjectExtensions.cs new file mode 100644 index 00000000..ade6ffa6 --- /dev/null +++ b/src/Subjects/Common.Extensions/System/ObjectExtensions.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Goodtocode.Common.Extensions; + +/// +/// object Extensions +/// +public static class ObjectExtension +{ + /// + /// Get list of properties decorated with the passed attribute + /// + /// + /// + /// + public static IEnumerable GetPropertiesByAttribute(this object item, Type myAttribute) + { + var returnValue = item.GetType().GetTypeInfo().DeclaredProperties.Where( + p => p.GetCustomAttributes(myAttribute, false).Any()); + + return returnValue; + } + + /// + /// Safe Type Casting based on .NET default() method + /// + /// default(DestinationType) + /// Item to default. + /// default(DestinationType) + public static TDestination? DefaultSafe(this object item) + { + var returnValue = TypeExtension.InvokeConstructorOrDefault(); + + try + { + if (item != null) + { + returnValue = (TDestination)item; + } + } + catch + { + returnValue = TypeExtension.InvokeConstructorOrDefault(); + } + + return returnValue; + } + + /// + /// Safe type casting via (TDestination)item method. + /// If cast fails, will return constructed object + /// + /// Type to default, or create new() + /// Item to cast + /// Cast result via (TDestination)item, or item.Fill(), or new TDestination(). + public static TDestination CastSafe(this object item) where TDestination : new() + { + var returnValue = new TDestination(); + + try + { + returnValue = item != null ? (TDestination)item : returnValue; + } + catch (InvalidCastException) + { + returnValue = new TDestination(); + } + + return returnValue; + } + + /// + /// Safe Type Casting based on Default.{Type} conventions. + /// If cast fails, will attempt the slower Fill() of data via reflection + /// + /// Type to default, or create new() + /// Item to cast + /// Defaulted type, or created new() + public static TDestination CastOrCopyProperties(this object item) where TDestination : new() + { + var returnValue = new TDestination(); + + try + { + returnValue = item != null ? (TDestination)item : returnValue; + } + catch (InvalidCastException) + { + returnValue.CopyPropertiesSafe(item); + } + + return returnValue; + } + + /// + /// Safe Type Casting based on Default.{Type} conventions. + /// If cast fails, will attempt the slower Fill() of data via reflection + /// + /// Type to default, or create new() + /// Item to cast + /// Defaulted type, or created new() + public static TDestination CopyPropertiesSafe(this object item) where TDestination : new() + { + var returnValue = new TDestination(); + returnValue.CopyPropertiesSafe(item); + return returnValue; + } + + /// + /// Item to exception-safe cast to string + /// + /// Item to cast + /// Converted string, or "" + public static string? ToStringSafe(this object item) + { + var returnValue = string.Empty; + + if (item == null == false) + { + returnValue = item.ToString(); + } + + return returnValue; + } + + + /// + /// Fills this object with another object's data, by matching property names + /// + /// Type of original object. + /// Destination object to fill + /// Source object + public static void CopyPropertiesSafe(this T item, object sourceItem) + { + var sourceType = sourceItem.GetType(); + + foreach (PropertyInfo sourceProperty in sourceType.GetRuntimeProperties()) + { + PropertyInfo? destinationProperty = typeof(T).GetRuntimeProperty(sourceProperty.Name); + if (destinationProperty != null && destinationProperty.CanWrite) + { + // Copy data only for Primitive-ish types including Value types, Guid, String, etc. + Type destinationPropertyType = destinationProperty.PropertyType; + if (destinationPropertyType.GetTypeInfo().IsPrimitive || destinationPropertyType.GetTypeInfo().IsValueType + || (destinationPropertyType == typeof(string)) || (destinationPropertyType == typeof(Guid))) + { + destinationProperty.SetValue(item, sourceProperty.GetValue(sourceItem, null), null); + } + } + } + } +} \ No newline at end of file diff --git a/src/Subjects/Common.Extensions/System/TypeExtensions.cs b/src/Subjects/Common.Extensions/System/TypeExtensions.cs new file mode 100644 index 00000000..6bff6439 --- /dev/null +++ b/src/Subjects/Common.Extensions/System/TypeExtensions.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Goodtocode.Common.Extensions; + +/// +/// Extends System.Type +/// +public static class TypeExtension +{ + /// + /// Invokes the parameterless constructor. If no parameterless constructor, returns default() + /// + /// Type to invoke + public static T? InvokeConstructorOrDefault() + { + var returnValue = default(T); + + if (TypeExtension.HasParameterlessConstructor()) + { + returnValue = Activator.CreateInstance(); + } + + return returnValue; + } + + /// + /// Determines if type has a parameterless constructor + /// + /// Type to interrogate for parameterless constructor + /// + public static bool HasParameterlessConstructor() + { + IEnumerable constructors = typeof(T).GetTypeInfo().DeclaredConstructors; + return constructors.Where(x => x.GetParameters().Count() == 0).Any(); + } + + /// + /// Determines if type has a parameterless constructor + /// + /// Type to interrogate for parameterless constructor + /// + public static bool HasParameterlessConstructor(this Type item) + { + IEnumerable constructors = item.GetTypeInfo().DeclaredConstructors; + return constructors.Where(x => x.GetParameters().Count() == 0).Any(); + } +} \ No newline at end of file diff --git a/src/Subjects/Common/Common.Domain/Common.Domain.csproj b/src/Subjects/Common/Common.Domain/Common.Domain.csproj index dd093679..f96ed9c8 100644 --- a/src/Subjects/Common/Common.Domain/Common.Domain.csproj +++ b/src/Subjects/Common/Common.Domain/Common.Domain.csproj @@ -9,4 +9,8 @@ enable + + + + diff --git a/src/Subjects/Common/Common.Domain/Extensions/ObjectExtensions.cs b/src/Subjects/Common/Common.Domain/Extensions/ObjectExtensions.cs deleted file mode 100644 index 781d975a..00000000 --- a/src/Subjects/Common/Common.Domain/Extensions/ObjectExtensions.cs +++ /dev/null @@ -1,157 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Goodtocode.Common.Extensions -{ - /// - /// object Extensions - /// - public static class ObjectExtension - { - /// - /// Get list of properties decorated with the passed attribute - /// - /// - /// - /// - public static IEnumerable GetPropertiesByAttribute(this object item, Type myAttribute) - { - var returnValue = item.GetType().GetTypeInfo().DeclaredProperties.Where( - p => p.GetCustomAttributes(myAttribute, false).Any()); - - return returnValue; - } - - /// - /// Safe Type Casting based on .NET default() method - /// - /// default(DestinationType) - /// Item to default. - /// default(DestinationType) - public static TDestination? DefaultSafe(this object item) - { - var returnValue = TypeExtension.InvokeConstructorOrDefault(); - - try - { - if (item != null) - { - returnValue = (TDestination)item; - } - } - catch - { - returnValue = TypeExtension.InvokeConstructorOrDefault(); - } - - return returnValue; - } - - /// - /// Safe type casting via (TDestination)item method. - /// If cast fails, will return constructed object - /// - /// Type to default, or create new() - /// Item to cast - /// Cast result via (TDestination)item, or item.Fill(), or new TDestination(). - public static TDestination CastSafe(this object item) where TDestination : new() - { - var returnValue = new TDestination(); - - try - { - returnValue = item != null ? (TDestination)item : returnValue; - } - catch (InvalidCastException) - { - returnValue = new TDestination(); - } - - return returnValue; - } - - /// - /// Safe Type Casting based on Default.{Type} conventions. - /// If cast fails, will attempt the slower Fill() of data via reflection - /// - /// Type to default, or create new() - /// Item to cast - /// Defaulted type, or created new() - public static TDestination CastOrCopyProperties(this object item) where TDestination : new() - { - var returnValue = new TDestination(); - - try - { - returnValue = item != null ? (TDestination)item : returnValue; - } - catch (InvalidCastException) - { - returnValue.CopyPropertiesSafe(item); - } - - return returnValue; - } - - /// - /// Safe Type Casting based on Default.{Type} conventions. - /// If cast fails, will attempt the slower Fill() of data via reflection - /// - /// Type to default, or create new() - /// Item to cast - /// Defaulted type, or created new() - public static TDestination CopyPropertiesSafe(this object item) where TDestination : new() - { - var returnValue = new TDestination(); - returnValue.CopyPropertiesSafe(item); - return returnValue; - } - - /// - /// Item to exception-safe cast to string - /// - /// Item to cast - /// Converted string, or "" - public static string? ToStringSafe(this object item) - { - var returnValue = string.Empty; - - if (item == null == false) - { - returnValue = item.ToString(); - } - - return returnValue; - } - - - /// - /// Fills this object with another object's data, by matching property names - /// - /// Type of original object. - /// Destination object to fill - /// Source object - public static void CopyPropertiesSafe(this T item, object sourceItem) - { - var sourceType = sourceItem.GetType(); - - foreach (PropertyInfo sourceProperty in sourceType.GetRuntimeProperties()) - { - PropertyInfo? destinationProperty = typeof(T).GetRuntimeProperty(sourceProperty.Name); - if (destinationProperty != null && destinationProperty.CanWrite) - { - // Copy data only for Primitive-ish types including Value types, Guid, String, etc. - Type destinationPropertyType = destinationProperty.PropertyType; - if (destinationPropertyType.GetTypeInfo().IsPrimitive || destinationPropertyType.GetTypeInfo().IsValueType - || (destinationPropertyType == typeof(string)) || (destinationPropertyType == typeof(Guid))) - { - destinationProperty.SetValue(item, sourceProperty.GetValue(sourceItem, null), null); - } - } - } - } - } -} \ No newline at end of file diff --git a/src/Subjects/Common/Common.Domain/Extensions/TypeExtensions.cs b/src/Subjects/Common/Common.Domain/Extensions/TypeExtensions.cs deleted file mode 100644 index f849a4bb..00000000 --- a/src/Subjects/Common/Common.Domain/Extensions/TypeExtensions.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; - -namespace Goodtocode.Common.Extensions -{ - /// - /// Extends System.Type - /// - public static class TypeExtension - { - /// - /// Invokes the parameterless constructor. If no parameterless constructor, returns default() - /// - /// Type to invoke - public static T? InvokeConstructorOrDefault() - { - var returnValue = default(T); - - if (TypeExtension.HasParameterlessConstructor()) - { - returnValue = Activator.CreateInstance(); - } - - return returnValue; - } - - /// - /// Determines if type has a parameterless constructor - /// - /// Type to interrogate for parameterless constructor - /// - public static bool HasParameterlessConstructor() - { - IEnumerable constructors = typeof(T).GetTypeInfo().DeclaredConstructors; - return constructors.Where(x => x.GetParameters().Count() == 0).Any(); - } - - /// - /// Determines if type has a parameterless constructor - /// - /// Type to interrogate for parameterless constructor - /// - public static bool HasParameterlessConstructor(this Type item) - { - IEnumerable constructors = item.GetTypeInfo().DeclaredConstructors; - return constructors.Where(x => x.GetParameters().Count() == 0).Any(); - } - } -} \ No newline at end of file diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs index f2a943e7..5caa3548 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs @@ -1,14 +1,16 @@ -using Goodtocode.Subjects.Domain; +using Goodtocode.Common.Extensions; +using Goodtocode.Subjects.Domain; using MediatR; namespace Goodtocode.Subjects.Application; -public class GetBusinessesByNameQuery : IRequest> +public class GetBusinessesByNameQuery : IRequest> { public string BusinessName { get; set; } = string.Empty; + public int Page { get; set; } = 1; } -public class GetBusinessesByNameQueryHandler : IRequestHandler> +public class GetBusinessesByNameQueryHandler : IRequestHandler> { private readonly IBusinessRepo _userBusinessesRepo; @@ -17,11 +19,11 @@ public GetBusinessesByNameQueryHandler(IBusinessRepo userBusinessesRepo) _userBusinessesRepo = userBusinessesRepo; } - public async Task> Handle(GetBusinessesByNameQuery request, + public async Task> Handle(GetBusinessesByNameQuery request, CancellationToken cancellationToken) { var businesses = - await _userBusinessesRepo.GetBusinessesByNameAsync(request.BusinessName, + await _userBusinessesRepo.GetBusinessesByNameAsync(request.BusinessName, request.Page, cancellationToken); return businesses.GetValueOrDefault(); diff --git a/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs b/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs index b89afc31..ce797664 100644 --- a/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs +++ b/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs @@ -1,4 +1,5 @@ using CSharpFunctionalExtensions; +using Goodtocode.Common.Extensions; using Goodtocode.Subjects.Domain; namespace Goodtocode.Subjects.Application; @@ -8,7 +9,7 @@ public interface IBusinessRepo Task> GetBusinessAsync(Guid businessKey, CancellationToken cancellationToken); - Task>> GetBusinessesByNameAsync(string businessName, + Task>> GetBusinessesByNameAsync(string businessName, int page, CancellationToken cancellationToken); Task> AddBusinessAsync(IBusinessObject businessInfo, diff --git a/src/Subjects/Goodtocode.Subjects.sln b/src/Subjects/Goodtocode.Subjects.sln index de760a01..2e5bc310 100644 --- a/src/Subjects/Goodtocode.Subjects.sln +++ b/src/Subjects/Goodtocode.Subjects.sln @@ -27,7 +27,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Specs.Integration", "Specs. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Specs.Unit", "Specs.Unit\Specs.Unit.csproj", "{B1912655-E309-4886-A882-1C384ACFB165}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation.Shared.Rcl", "Presentation.Shared.Rcl\Presentation.Shared.Rcl.csproj", "{52F8BCD2-BB2D-4A23-8638-BBDC777D167B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Presentation.Shared.Rcl", "Presentation.Shared.Rcl\Presentation.Shared.Rcl.csproj", "{52F8BCD2-BB2D-4A23-8638-BBDC777D167B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Extensions", "Common.Extensions\Common.Extensions.csproj", "{1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -83,6 +85,10 @@ Global {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Debug|Any CPU.Build.0 = Debug|Any CPU {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Release|Any CPU.ActiveCfg = Release|Any CPU {52F8BCD2-BB2D-4A23-8638-BBDC777D167B}.Release|Any CPU.Build.0 = Release|Any CPU + {1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -91,6 +97,7 @@ Global {D5BF79A6-68B3-41B0-AB63-28E30DD87018} = {5D8887CA-2929-4E91-9409-B3366F968527} {DCD762D0-8543-4436-969C-975A94833F20} = {5D8887CA-2929-4E91-9409-B3366F968527} {38DDD951-3F89-41FE-A225-326DA46A39CF} = {5D8887CA-2929-4E91-9409-B3366F968527} + {1301D19A-3E5A-4EA2-BFC0-625DAA7436AC} = {5D8887CA-2929-4E91-9409-B3366F968527} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {64B2FD6C-D2E4-4D45-B05C-684B31797448} diff --git a/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs b/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs new file mode 100644 index 00000000..5b395666 --- /dev/null +++ b/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore; + +namespace Goodtocode.Common.Extensions; + +public static class IQueryableExtensions +{ + public static async Task> GetPagedAsync(this IQueryable query, int page, int pageSize, CancellationToken cancellationToken) where T : class + { + var result = new PagedResult(); + result.CurrentPage = page; + result.PageSize = pageSize; + result.RowCount = query.Count(); + + var pageCount = (double)result.RowCount / pageSize; + result.PageCount = (int)Math.Ceiling(pageCount); + + var skip = (page - 1) * pageSize; + result.Results = await query.Skip(skip).Take(pageSize).ToListAsync(cancellationToken); + + return result; + } +} \ No newline at end of file diff --git a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs index 226c8263..0502cd2a 100644 --- a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs +++ b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs @@ -1,4 +1,5 @@ using CSharpFunctionalExtensions; +using Goodtocode.Common.Extensions; using Goodtocode.Subjects.Application; using Goodtocode.Subjects.Domain; using Microsoft.Data.SqlClient; @@ -10,10 +11,12 @@ namespace Goodtocode.Subjects.Persistence.Repositories; public class BusinessRepo : IBusinessRepo { private readonly ISubjectsDbContext _context; + private readonly int _pageSize = 20; - public BusinessRepo(ISubjectsDbContext context) + public BusinessRepo(ISubjectsDbContext context, int pageSize = 20) { _context = context; + _pageSize = pageSize; } public async Task> GetBusinessAsync(Guid businessKey, CancellationToken cancellationToken) @@ -25,9 +28,12 @@ public BusinessRepo(ISubjectsDbContext context) return Result.Failure("Business not found."); } - public async Task>> GetBusinessesByNameAsync(string businessName, CancellationToken cancellationToken) + public async Task>> GetBusinessesByNameAsync(string businessName, int page, CancellationToken cancellationToken) { - var businessResult = await _context.Business.Where(x => x.BusinessName == businessName).ToListAsync(cancellationToken); + var businessResult = await _context.Business + .Where(b => b.BusinessName.Contains(businessName, StringComparison.CurrentCultureIgnoreCase)) + .OrderBy(b => b.BusinessKey) + .GetPagedAsync(page, _pageSize, cancellationToken); return Result.Success(businessResult); } diff --git a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs index 9c210bbe..7d564e80 100644 --- a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs +++ b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs @@ -1,4 +1,5 @@ -using Goodtocode.Subjects.Application; +using Goodtocode.Common.Extensions; +using Goodtocode.Subjects.Application; using Goodtocode.Subjects.Domain; using Goodtocode.Subjects.WebApi.Common; using Microsoft.AspNetCore.Mvc; @@ -24,7 +25,7 @@ public class BusinessesController : BaseController [HttpGet(Name = "GetBusinessesByNameQuery")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public async Task> Get(string name, int pageNumber = 1, int pageSize = 20) => await Mediator.Send(new GetBusinessesByNameQuery + public async Task> Get(string name, int pageNumber = 1, int pageSize = 20) => await Mediator.Send(new GetBusinessesByNameQuery { BusinessName = name }); diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs b/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs deleted file mode 100644 index 5a9fbd76..00000000 --- a/src/Subjects/Presentation.Shared.Rcl/Paging/PagedResultExtension.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Goodtocode.Subjects.Rcl; - -public static class PagedResultExtensions -{ - public static PagedResult GetPaged(this IQueryable query, int page, int pageSize) where T : class - { - var result = new PagedResult(); - result.CurrentPage = page; - result.PageSize = pageSize; - result.RowCount = query.Count(); - - var pageCount = (double)result.RowCount / pageSize; - result.PageCount = (int)Math.Ceiling(pageCount); - - var skip = (page - 1) * pageSize; - result.Results = query.Skip(skip).Take(pageSize).ToList(); - - return result; - } -} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor index 75d5c791..bf44a977 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor @@ -1,3 +1,4 @@ +@using Goodtocode.Common.Extensions; @if (Result != null) {
diff --git a/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj b/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj index e91e9c17..0b7ee88e 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj +++ b/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj @@ -17,4 +17,8 @@ + + + + diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index 5a758bc5..0938b392 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -1,4 +1,5 @@ @page "/businesssearch" +@using Goodtocode.Common.Extensions; @using Goodtocode.Subjects.BlazorServer.Data; @using Goodtocode.Subjects.BlazorServer.Models; @using Goodtocode.Subjects.Domain; diff --git a/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs index 0ce84736..02537d4c 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/IBusinessService.cs @@ -1,7 +1,5 @@ -using Goodtocode.Subjects.BlazorServer.Models; -using Goodtocode.Subjects.Domain; -using Goodtocode.Subjects.Rcl; -using System; +using Goodtocode.Common.Extensions; +using Goodtocode.Subjects.BlazorServer.Models; namespace Goodtocode.Subjects.BlazorServer.Data { diff --git a/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs b/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs index 8af7fecd..579eca83 100644 --- a/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs +++ b/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs @@ -1,5 +1,6 @@ using CSharpFunctionalExtensions; using FluentValidation.Results; +using Goodtocode.Common.Extensions; using Goodtocode.Subjects.Application; using Goodtocode.Subjects.Application.Common.Exceptions; using Goodtocode.Subjects.Domain; @@ -14,7 +15,7 @@ public class GetBusinessesByNameStepDefinitions : TestBase { private IDictionary _commandErrors = new ConcurrentDictionary(); private string[]? _expectedInvalidFields; - private List _response = new(); + private PagedResult _response = new(); private CommandResponseType _responseType; private ValidationResult _validationErrors = new(); private string _businessName = string.Empty; @@ -108,13 +109,13 @@ public void ThenIfTheResponseHasValidationIssuesISeeTheInTheResponse(string resp public void ThenIfTheResponseIsValidThenTheResponseContainsACollectionOfBusinesses() { if (_responseType != CommandResponseType.Successful) return; - _response.Any().Should().BeTrue(); + _response.Results.Any().Should().BeTrue(); } [Then(@"each business has a matching BusinessName of ""([^""]*)""")] public void ThenEachBusinessHasAMatchingBusinessNameOf(string businessInDb) { if (_responseType != CommandResponseType.Successful) return; - foreach (var business in _response) business.BusinessName.Should().Be(businessInDb); + foreach (var business in _response.Results) business.BusinessName.Should().Be(businessInDb); } } \ No newline at end of file diff --git a/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs b/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs index 884f6628..29aabcff 100644 --- a/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs +++ b/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs @@ -1,5 +1,6 @@ using CSharpFunctionalExtensions; using FluentValidation.Results; +using Goodtocode.Common.Extensions; using Goodtocode.Subjects.Application; using Goodtocode.Subjects.Application.Common.Exceptions; using Goodtocode.Subjects.Domain; @@ -15,7 +16,7 @@ public class GetBusinessesByNameStepDefinitions : TestBase { private IDictionary _commandErrors = new ConcurrentDictionary(); private string[]? _expectedInvalidFields; - private List _response = new(); + private PagedResult _response = new(); private CommandResponseType _responseType; private ValidationResult _validationErrors = new(); private string _businessName = string.Empty; @@ -47,15 +48,17 @@ public async Task WhenIQueryForMatchingBusinesses() if (_businessExists) { userBusinessesRepoMock - .Setup(x => x.GetBusinessesByNameAsync(_businessName, It.IsAny())) - .Returns(Task.FromResult(Result.Success(new List - { + .Setup(x => x.GetBusinessesByNameAsync(_businessName, 1, It.IsAny())) + .Returns(Task.FromResult(Result.Success(new PagedResult( new() { + new() + { BusinessKey = new Guid(), BusinessName = "BusinessInDb" - } - }))); + } + }) + ))); } var request = new GetBusinessesByNameQuery @@ -133,13 +136,13 @@ public void ThenIfTheResponseHasValidationIssuesISeeTheInTheResponse(string resp public void ThenIfTheResponseIsValidThenTheResponseContainsACollectionOfBusinesses() { if (_responseType != CommandResponseType.Successful) return; - _response.Any().Should().BeTrue(); + _response.Results.Any().Should().BeTrue(); } [Then(@"each business has a matching BusinessName of ""([^""]*)""")] public void ThenEachBusinessHasAMatchingBusinessNameOf(string businessInDb) { if (_responseType != CommandResponseType.Successful) return; - foreach (var business in _response) business.BusinessName.Should().Be(businessInDb); + foreach (var business in _response.Results) business.BusinessName.Should().Be(businessInDb); } } \ No newline at end of file From 25a7e5969fec1c6a810283942b1995c48256be8a Mon Sep 17 00:00:00 2001 From: Robert Good Date: Wed, 28 Jun 2023 21:34:04 -0700 Subject: [PATCH 04/25] basic pagination wired, not pulling data --- .../Pages/Business/BusinessList.razor | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index 0938b392..e5c6462c 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -13,7 +13,7 @@ Business Search - +
(), true)) return; @@ -136,6 +128,7 @@ finally { processing = false; + StateHasChanged(); } } From f4873b649b94121068941640b657ef9424bcf0f4 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Fri, 30 Jun 2023 21:42:01 -0700 Subject: [PATCH 05/25] query flaw in contains --- .../Repositories/BusinessRepo.cs | 3 ++- .../Models/SearchModel.cs | 10 ++++++++++ .../Pages/Business/BusinessList.razor | 5 ++--- .../Services/BusinessService.cs | 2 +- 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Models/SearchModel.cs diff --git a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs index 0502cd2a..a393c966 100644 --- a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs +++ b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs @@ -31,7 +31,8 @@ public BusinessRepo(ISubjectsDbContext context, int pageSize = 20) public async Task>> GetBusinessesByNameAsync(string businessName, int page, CancellationToken cancellationToken) { var businessResult = await _context.Business - .Where(b => b.BusinessName.Contains(businessName, StringComparison.CurrentCultureIgnoreCase)) + //.Where(b => b.BusinessName.Contains(businessName, StringComparison.CurrentCultureIgnoreCase)) + .Where(b => b.BusinessName == businessName) .OrderBy(b => b.BusinessKey) .GetPagedAsync(page, _pageSize, cancellationToken); return Result.Success(businessResult); diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/SearchModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/SearchModel.cs new file mode 100644 index 00000000..4348b303 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/SearchModel.cs @@ -0,0 +1,10 @@ +using Goodtocode.Subjects.Domain; +using System.ComponentModel.DataAnnotations; + +namespace Goodtocode.Subjects.BlazorServer.Models; + +public class SearchModel +{ + [Required] + public string Name { get; set; } = string.Empty; +} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index e5c6462c..4297fbcb 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -74,10 +74,9 @@ [Parameter] public int Page { get; set; } = 1; [Parameter] - [Required] public string SearchTerm { get; set; } = string.Empty; private string alertMessage = string.Empty; - private BusinessModel businessSearch = new BusinessModel(); + private SearchModel businessSearch = new SearchModel(); private PagedResult businesses = new PagedResult(); private CancellationTokenSource cts = new CancellationTokenSource(); private bool processing; @@ -90,7 +89,7 @@ protected override async Task OnParametersSetAsync() { - businesses = await Service.GetBusinessesAsync(null, Page); + await GetBusinesses(); PageHistory.AddPageToHistory(UriHelper.Uri); } diff --git a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs index adae03de..a8627f3a 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs @@ -35,7 +35,7 @@ public async Task> GetBusinessesAsync(string name, in { var business = new PagedResult(); var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.GetAsync($"{httpClient.BaseAddress}/Businesses?name={name}&api-version=1"); + var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Businesses?name={name}&pageNumber=1&pageSize=20&api-version=1"); if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); From 82fda7561ae7df24755e8cce3f7c55f8eb1701fa Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 14:38:28 -0700 Subject: [PATCH 06/25] Condensed project names, added dockerfile --- .../AccessToken.cs | 12 +++---- .../Common/Common.ApiClient/BearerToken.cs | 13 ++++++++ .../BearerTokenHandler.cs | 0 .../ClientCredentialSetting.cs | 0 .../Common.ApiClient.csproj} | 4 +-- .../ConfigureServices.cs | 0 .../Common/Common.Domain/Common.Domain.csproj | 2 +- .../Common.Extensions.csproj | 0 .../PagedResult/PagedResultBase.cs | 0 .../PagedResult/PagedResultT.cs | 0 .../System/ObjectExtensions.cs | 0 .../System/TypeExtensions.cs | 0 .../BearerToken.cs | 12 ------- src/Subjects/Dockerfile | 32 +++++++++++++------ src/Subjects/Goodtocode.Subjects.sln | 6 ++-- .../Presentation.Web.BlazorServer.csproj | 2 +- .../GetBusinessesByNameStepDefinitions.cs | 13 ++++++-- src/Subjects/docker-compose.yml | 4 +-- 18 files changed, 59 insertions(+), 41 deletions(-) rename src/Subjects/Common/{Common.Infrastructure.ApiClient => Common.ApiClient}/AccessToken.cs (88%) create mode 100644 src/Subjects/Common/Common.ApiClient/BearerToken.cs rename src/Subjects/Common/{Common.Infrastructure.ApiClient => Common.ApiClient}/BearerTokenHandler.cs (100%) rename src/Subjects/Common/{Common.Infrastructure.ApiClient => Common.ApiClient}/ClientCredentialSetting.cs (100%) rename src/Subjects/Common/{Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj => Common.ApiClient/Common.ApiClient.csproj} (83%) rename src/Subjects/Common/{Common.Infrastructure.ApiClient => Common.ApiClient}/ConfigureServices.cs (100%) rename src/Subjects/{ => Common}/Common.Extensions/Common.Extensions.csproj (100%) rename src/Subjects/{ => Common}/Common.Extensions/PagedResult/PagedResultBase.cs (100%) rename src/Subjects/{ => Common}/Common.Extensions/PagedResult/PagedResultT.cs (100%) rename src/Subjects/{ => Common}/Common.Extensions/System/ObjectExtensions.cs (100%) rename src/Subjects/{ => Common}/Common.Extensions/System/TypeExtensions.cs (100%) delete mode 100644 src/Subjects/Common/Common.Infrastructure.ApiClient/BearerToken.cs diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/AccessToken.cs b/src/Subjects/Common/Common.ApiClient/AccessToken.cs similarity index 88% rename from src/Subjects/Common/Common.Infrastructure.ApiClient/AccessToken.cs rename to src/Subjects/Common/Common.ApiClient/AccessToken.cs index 426b159f..5697b4b2 100644 --- a/src/Subjects/Common/Common.Infrastructure.ApiClient/AccessToken.cs +++ b/src/Subjects/Common/Common.ApiClient/AccessToken.cs @@ -1,4 +1,5 @@ -using RestSharp; +using Microsoft.IdentityModel.Tokens; +using RestSharp; using System.Text.Json; namespace Goodtocode.Common.Infrastructure.ApiClient; @@ -53,15 +54,10 @@ private async Task GetNewAccessToken() var restClient = new RestClient(tokenUrl); var response = await restClient.ExecuteAsync(request, CancellationToken.None); - if (!response.IsSuccessful) return string.Empty; + if (!response.IsSuccessful || string.IsNullOrEmpty(response.Content)) return string.Empty; var tokenResponse = JsonSerializer.Deserialize(response.Content); + if (tokenResponse == null) return string.Empty; ExpirationDateUtc = DateTime.UtcNow.AddSeconds(tokenResponse.expires_in); return tokenResponse.access_token; } - - private void SetAccessToken(string accessToken, DateTime expirationDate) - { - Token = accessToken; - ExpirationDateUtc = expirationDate; - } } \ No newline at end of file diff --git a/src/Subjects/Common/Common.ApiClient/BearerToken.cs b/src/Subjects/Common/Common.ApiClient/BearerToken.cs new file mode 100644 index 00000000..3cfde3a5 --- /dev/null +++ b/src/Subjects/Common/Common.ApiClient/BearerToken.cs @@ -0,0 +1,13 @@ +namespace Goodtocode.Common.Infrastructure.ApiClient; + +[System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "RFC6749")] +public class BearerToken +{ + public string token_type { get; set; } = string.Empty; + + public int expires_in { get; set; } + + public int ext_expires_in { get; set; } + + public string access_token { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/BearerTokenHandler.cs b/src/Subjects/Common/Common.ApiClient/BearerTokenHandler.cs similarity index 100% rename from src/Subjects/Common/Common.Infrastructure.ApiClient/BearerTokenHandler.cs rename to src/Subjects/Common/Common.ApiClient/BearerTokenHandler.cs diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/ClientCredentialSetting.cs b/src/Subjects/Common/Common.ApiClient/ClientCredentialSetting.cs similarity index 100% rename from src/Subjects/Common/Common.Infrastructure.ApiClient/ClientCredentialSetting.cs rename to src/Subjects/Common/Common.ApiClient/ClientCredentialSetting.cs diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj b/src/Subjects/Common/Common.ApiClient/Common.ApiClient.csproj similarity index 83% rename from src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj rename to src/Subjects/Common/Common.ApiClient/Common.ApiClient.csproj index f7b8421b..7de2cbf4 100644 --- a/src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj +++ b/src/Subjects/Common/Common.ApiClient/Common.ApiClient.csproj @@ -1,8 +1,8 @@  - Goodtocode.Common.Infrastructure.ApiClient - Goodtocode.Common.Infrastructure.ApiClient + Goodtocode.Common.ApiClient + Goodtocode.Common.ApiClient 1.0.0 net7.0 enable diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/ConfigureServices.cs b/src/Subjects/Common/Common.ApiClient/ConfigureServices.cs similarity index 100% rename from src/Subjects/Common/Common.Infrastructure.ApiClient/ConfigureServices.cs rename to src/Subjects/Common/Common.ApiClient/ConfigureServices.cs diff --git a/src/Subjects/Common/Common.Domain/Common.Domain.csproj b/src/Subjects/Common/Common.Domain/Common.Domain.csproj index f96ed9c8..3c6d4f3e 100644 --- a/src/Subjects/Common/Common.Domain/Common.Domain.csproj +++ b/src/Subjects/Common/Common.Domain/Common.Domain.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Subjects/Common.Extensions/Common.Extensions.csproj b/src/Subjects/Common/Common.Extensions/Common.Extensions.csproj similarity index 100% rename from src/Subjects/Common.Extensions/Common.Extensions.csproj rename to src/Subjects/Common/Common.Extensions/Common.Extensions.csproj diff --git a/src/Subjects/Common.Extensions/PagedResult/PagedResultBase.cs b/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultBase.cs similarity index 100% rename from src/Subjects/Common.Extensions/PagedResult/PagedResultBase.cs rename to src/Subjects/Common/Common.Extensions/PagedResult/PagedResultBase.cs diff --git a/src/Subjects/Common.Extensions/PagedResult/PagedResultT.cs b/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultT.cs similarity index 100% rename from src/Subjects/Common.Extensions/PagedResult/PagedResultT.cs rename to src/Subjects/Common/Common.Extensions/PagedResult/PagedResultT.cs diff --git a/src/Subjects/Common.Extensions/System/ObjectExtensions.cs b/src/Subjects/Common/Common.Extensions/System/ObjectExtensions.cs similarity index 100% rename from src/Subjects/Common.Extensions/System/ObjectExtensions.cs rename to src/Subjects/Common/Common.Extensions/System/ObjectExtensions.cs diff --git a/src/Subjects/Common.Extensions/System/TypeExtensions.cs b/src/Subjects/Common/Common.Extensions/System/TypeExtensions.cs similarity index 100% rename from src/Subjects/Common.Extensions/System/TypeExtensions.cs rename to src/Subjects/Common/Common.Extensions/System/TypeExtensions.cs diff --git a/src/Subjects/Common/Common.Infrastructure.ApiClient/BearerToken.cs b/src/Subjects/Common/Common.Infrastructure.ApiClient/BearerToken.cs deleted file mode 100644 index 988d72f9..00000000 --- a/src/Subjects/Common/Common.Infrastructure.ApiClient/BearerToken.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Goodtocode.Common.Infrastructure.ApiClient; - -public class BearerToken -{ - public string token_type { get; set; } - - public int expires_in { get; set; } - - public int ext_expires_in { get; set; } - - public string access_token { get; set; } -} \ No newline at end of file diff --git a/src/Subjects/Dockerfile b/src/Subjects/Dockerfile index 7676027b..e42b7fa8 100644 --- a/src/Subjects/Dockerfile +++ b/src/Subjects/Dockerfile @@ -3,21 +3,33 @@ FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env WORKDIR /src ### Copy csproj and sln and restore as distinct layers -COPY Blazorcrud.Client/*.csproj Blazorcrud.Client/ -COPY Blazorcrud.Server/*.csproj Blazorcrud.Server/ -COPY Blazorcrud.Shared/*.csproj Blazorcrud.Shared/ -COPY Blazorcrud.sln . +COPY Common/Common.ApiClient/*.csproj Common/Common.ApiClient/ +COPY Common/Common.Application/*.csproj Common/Common.Application/ +COPY Common/Common.Domain/*.csproj Common/Common.Domain/ +COPY Common/Common.Extensions/*.csproj Common/Common.Extensions/ +COPY Core.Application/*.csproj Core.Application/ +COPY Core.Domain/*.csproj Core.Domain/ +COPY Infrastructure.Persistence/*.csproj Infrastructure.Persistence/ +COPY Presentation.Api.WebApi/*.csproj Presentation.Api.WebApi/ +COPY Presentation.Shared.Rcl/*.csproj Presentation.Shared.Rcl/ +COPY Presentation.Web.BlazorServer/*.csproj Presentation.Web.BlazorServer/ +COPY Presentation.Web.BlazorStatic/*.csproj Presentation.Web.BlazorStatic/ +COPY Goodtocode.Subjects.sln . RUN dotnet restore ### PUBLISH FROM build-env as publish-env COPY . . -RUN dotnet publish "Blazorcrud.sln" -c Release -o /app +RUN dotnet publish "Goodtocode.Subjects.sln" -c Release -o /app/out ### RUNTIME IMAGE FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime-env -WORKDIR /app -COPY --from=publish-env /app . -ENV ASPNETCORE_URLS=http://+:80 -EXPOSE 80 -ENTRYPOINT ["dotnet", "Blazorcrud.Server.dll"] \ No newline at end of file +WORKDIR /app/out +COPY --from=publish-env /app/out . + +ENV ASPNETCORE_URLS=http://+:30001 +EXPOSE 30001 + +ENV ASPNETCORE_URLS=http://+:30002 +EXPOSE 30002 +ENTRYPOINT ["dotnet", "Goodtocode.Subjects.WebApi.dll", "--urls", "http://*:30002", "Goodtocode.Subjects.BlazorServer.dll", "--urls", "http://*:30001"] \ No newline at end of file diff --git a/src/Subjects/Goodtocode.Subjects.sln b/src/Subjects/Goodtocode.Subjects.sln index 2e5bc310..ef4ee785 100644 --- a/src/Subjects/Goodtocode.Subjects.sln +++ b/src/Subjects/Goodtocode.Subjects.sln @@ -9,7 +9,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Application", "Commo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Domain", "Common\Common.Domain\Common.Domain.csproj", "{DCD762D0-8543-4436-969C-975A94833F20}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.Infrastructure.ApiClient", "Common\Common.Infrastructure.ApiClient\Common.Infrastructure.ApiClient.csproj", "{38DDD951-3F89-41FE-A225-326DA46A39CF}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Extensions", "Common\Common.Extensions\Common.Extensions.csproj", "{1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common.ApiClient", "Common\Common.ApiClient\Common.ApiClient.csproj", "{38DDD951-3F89-41FE-A225-326DA46A39CF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core.Application", "Core.Application\Core.Application.csproj", "{859E4300-2DB5-431A-9319-5AEA12552D85}" EndProject @@ -29,8 +31,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Specs.Unit", "Specs.Unit\Sp EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Presentation.Shared.Rcl", "Presentation.Shared.Rcl\Presentation.Shared.Rcl.csproj", "{52F8BCD2-BB2D-4A23-8638-BBDC777D167B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common.Extensions", "Common.Extensions\Common.Extensions.csproj", "{1301D19A-3E5A-4EA2-BFC0-625DAA7436AC}" -EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj b/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj index 7f8a7c88..97cae788 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj +++ b/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj @@ -27,7 +27,7 @@ - + diff --git a/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs b/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs index 579eca83..1bce4ab5 100644 --- a/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs +++ b/src/Subjects/Specs.Integration/Business/GetBusinessesByNameStepDefinitions.cs @@ -19,11 +19,12 @@ public class GetBusinessesByNameStepDefinitions : TestBase private CommandResponseType _responseType; private ValidationResult _validationErrors = new(); private string _businessName = string.Empty; + private bool _businessExists; [Given(@"I have a def ""([^""]*)""")] - public void GivenIHaveADef(string p0) + public void GivenIHaveADef(string def) { - _def = p0; + _def = def; } [Given(@"I have a BusinessName ""([^""]*)""")] @@ -32,6 +33,13 @@ public void GivenIHaveABusinessName(string businessInDb) _businessName = businessInDb; } + [Given(@"the business exists ""([^""]*)""")] + public void GivenTheBusinessExists(string exists) + { + _businessExists = bool.Parse(exists); + } + + [When(@"I query for matching Businesses")] public async Task WhenIQueryForMatchingBusinesses() { @@ -84,6 +92,7 @@ public void ThenTheResponseIs(string response) break; case "NotFound": _responseType.Should().Be(CommandResponseType.NotFound); + _businessExists.Should().Be(false); break; } } diff --git a/src/Subjects/docker-compose.yml b/src/Subjects/docker-compose.yml index e957e081..bd7180de 100644 --- a/src/Subjects/docker-compose.yml +++ b/src/Subjects/docker-compose.yml @@ -1,7 +1,7 @@ version: "3.9" services: - blazorcrud: - image: thbst16/blazor-crud + entities: + image: goodtocode/entities build: . ports: - 8080:80 \ No newline at end of file From b00d303fc2a6dc7ce1287a52b17db82945d97b94 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 15:07:29 -0700 Subject: [PATCH 07/25] Contains working --- .../Infrastructure.Persistence/Repositories/BusinessRepo.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs index a393c966..956595de 100644 --- a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs +++ b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs @@ -31,8 +31,7 @@ public BusinessRepo(ISubjectsDbContext context, int pageSize = 20) public async Task>> GetBusinessesByNameAsync(string businessName, int page, CancellationToken cancellationToken) { var businessResult = await _context.Business - //.Where(b => b.BusinessName.Contains(businessName, StringComparison.CurrentCultureIgnoreCase)) - .Where(b => b.BusinessName == businessName) + .Where(b => b.BusinessName.Contains(businessName)) .OrderBy(b => b.BusinessKey) .GetPagedAsync(page, _pageSize, cancellationToken); return Result.Success(businessResult); From 931c1e67d00e09bc6523197a6d7cce0380eb4f80 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 16:05:50 -0700 Subject: [PATCH 08/25] List and icon styling improved --- .../Pages/Business/BusinessList.razor | 30 +++++++------------ .../Services/BusinessService.cs | 3 +- .../wwwroot/css/site.css | 8 +++++ 3 files changed, 19 insertions(+), 22 deletions(-) diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index 4297fbcb..462e5dea 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -30,14 +30,13 @@ @if (businesses.Results.Count() > 0) { -
Key Name Tax Number
@business.BusinessKey @business.BusinessName @business.TaxNumber + + + + @if (business.IsDeleting) + { + + } + else + { + + } +
+
- - + @@ -47,22 +46,13 @@ - } @@ -104,7 +94,7 @@ private async Task GetBusinesses() { alertMessage = string.Empty; - + businessSearch.Name = SearchTerm; if (!Validator.TryValidateObject(businessSearch, diff --git a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs index a8627f3a..86cae7c5 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs @@ -39,8 +39,7 @@ public async Task> GetBusinessesAsync(string name, in if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); - business = new PagedResult(JsonSerializer.Deserialize>(response.Content.ReadAsStream()) - ?? throw new Exception("Deserialization failed.")); + business = JsonSerializer.Deserialize>(response.Content.ReadAsStream()) ?? throw new Exception("Deserialization failed."); } return business; diff --git a/src/Subjects/Presentation.Web.BlazorServer/wwwroot/css/site.css b/src/Subjects/Presentation.Web.BlazorServer/wwwroot/css/site.css index 3cb57ea0..8d2d0487 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/wwwroot/css/site.css +++ b/src/Subjects/Presentation.Web.BlazorServer/wwwroot/css/site.css @@ -12,6 +12,14 @@ a, .btn-link { color: #0071c1; } +.nounderline { + text-decoration: none !important +} + +.nowrap { + white-space: nowrap +} + .btn-primary { color: #fff; background-color: #1b6ec2; From 804b2b832bae08c8b8f21309e90407c45416d627 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 16:19:11 -0700 Subject: [PATCH 09/25] wired delete --- .../Common/PagedResultExtensions.cs | 10 ++-- .../Pages/Business/BusinessForm.razor | 2 +- .../Pages/Business/BusinessList.razor | 47 ++++++++++++------- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs b/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs index 5b395666..dd7b41a6 100644 --- a/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs +++ b/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs @@ -6,10 +6,12 @@ public static class IQueryableExtensions { public static async Task> GetPagedAsync(this IQueryable query, int page, int pageSize, CancellationToken cancellationToken) where T : class { - var result = new PagedResult(); - result.CurrentPage = page; - result.PageSize = pageSize; - result.RowCount = query.Count(); + var result = new PagedResult + { + CurrentPage = page, + PageSize = pageSize, + RowCount = query.Count() + }; var pageCount = (double)result.RowCount / pageSize; result.PageCount = (int)Math.Ceiling(pageCount); diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor index 7844e35f..79c2c490 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor @@ -68,7 +68,7 @@ @code { [Parameter] - public BusinessModel business { get; set; } + public BusinessModel business { get; set; } = new BusinessModel(); [Parameter] public string ButtonText { get; set; } = "Save"; [Parameter] diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index 462e5dea..82fae643 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -13,7 +13,7 @@ Business Search - +
@business.BusinessName
Key Name Tax Number
@business.BusinessKey @business.BusinessName @business.TaxNumber - - - - @if (business.IsDeleting) - { - - } - else - { - - } + + + +
@business.TaxNumber - - - @@ -79,7 +79,7 @@ protected override async Task OnParametersSetAsync() { - await GetBusinesses(); + await GetBusinessesAsync(); PageHistory.AddPageToHistory(UriHelper.Uri); } @@ -87,11 +87,11 @@ { if (ev.Key == "Enter") { - await GetBusinesses(); + await GetBusinessesAsync(); } } - private async Task GetBusinesses() + private async Task GetBusinessesAsync() { alertMessage = string.Empty; @@ -106,7 +106,7 @@ { processing = true; await Task.Delay(500, cts.Token); - businesses = await Service.GetBusinessesAsync(businessSearch.Name, 1); + businesses = await Service.GetBusinessesAsync(businessSearch.Name, Page); if (businesses.Results.Count() == 0) alertMessage = "No businesses found"; } @@ -121,17 +121,28 @@ } } - private async Task DeleteBusiness(Guid businessKey) + private async Task DeleteBusinessAsync(BusinessModel business) { + alertMessage = string.Empty; + if (cts != null) cts.Cancel(); + cts = new CancellationTokenSource(); + try + { + processing = true; + var businessToDelete = business; + await Task.Delay(500, cts.Token); + await Service.DeleteBusinessAsync(business.BusinessKey); + await GetBusinessesAsync(); + } + catch (TaskCanceledException) + { + // Ignore exception if task was cancelled + } + finally + { + processing = false; + StateHasChanged(); + } } - - //private async void DeletePerson(Person _person) - //{ - // var person = _person; - // person.IsDeleting = true; - // await PersonService.DeletePerson(person.PersonId); - // people = await PersonService.GetPeople(null, Page); - // StateHasChanged(); - //} } From bf294bb93323e427a1095dd5279a5e34cc864553 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 21:07:11 -0700 Subject: [PATCH 10/25] Delete fixed --- .../Controllers/BusinessController.cs | 8 ++++---- .../Services/BusinessService.cs | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs index 784c84b9..148ce190 100644 --- a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs +++ b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs @@ -95,11 +95,11 @@ public async Task Put([FromBody] BusinessObject business) [ProducesResponseType(StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Post(Guid businessKey, [FromBody] BusinessObject business) + public async Task Post(Guid key, [FromBody] BusinessObject business) { var command = business.CopyPropertiesSafe(); - command.BusinessKey = businessKey; + command.BusinessKey = key; await Mediator.Send(command); return Ok(); @@ -120,12 +120,12 @@ public async Task Post(Guid businessKey, [FromBody] BusinessObject [HttpDelete(Name = "DeleteBusinessCommand")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task Delete(Guid businessKey) + public async Task Delete(Guid key) { var command = new DeleteBusinessCommand { - BusinessKey = businessKey + BusinessKey = key }; await Mediator.Send(command); diff --git a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs index 86cae7c5..5d7dcd36 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs @@ -10,6 +10,7 @@ namespace Goodtocode.Subjects.BlazorServer.Data; public class BusinessService : IBusinessService { private readonly IHttpClientFactory _clientFactory; + private const int apiVersion = 1; public BusinessService(IHttpClientFactory clientFactory) { @@ -19,7 +20,7 @@ public BusinessService(IHttpClientFactory clientFactory) public async Task GetBusinessAsync(Guid businessKey) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.GetAsync($"{httpClient.BaseAddress}/Business?key={businessKey}&api-version=1"); + var response = await httpClient.GetAsync($"{httpClient.BaseAddress}/Business?key={businessKey}&api-version={apiVersion}"); var business = new BusinessModel(); if (response.StatusCode != HttpStatusCode.NotFound) { @@ -35,7 +36,7 @@ public async Task> GetBusinessesAsync(string name, in { var business = new PagedResult(); var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Businesses?name={name}&pageNumber=1&pageSize=20&api-version=1"); + var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Businesses?name={name}&pageNumber=1&pageSize=20&api-version={apiVersion}"); if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); @@ -55,14 +56,14 @@ public async Task CreateBusinessAsync(BusinessModel business) public async Task UpdateBusinessAsync(BusinessModel business) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.PostAsJsonAsync($"{httpClient.BaseAddress}/Business?key={business.BusinessKey}api-version=1", business.CopyPropertiesSafe()); + var response = await httpClient.PostAsJsonAsync($"{httpClient.BaseAddress}/Business?key={business.BusinessKey}api-version={apiVersion}", business.CopyPropertiesSafe()); response.EnsureSuccessStatusCode(); } public async Task DeleteBusinessAsync(Guid businessKey) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.DeleteAsync($"{httpClient.BaseAddress}/Business?key={businessKey}api-version=1"); + var response = await httpClient.DeleteAsync($"{httpClient.BaseAddress}/Business?key={businessKey}api-version={apiVersion}"); response.EnsureSuccessStatusCode(); } } \ No newline at end of file From da537879b9979246f193709b90cc1600542d2e32 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 21:37:41 -0700 Subject: [PATCH 11/25] Fixed validation on create --- .../Presentation.Web.BlazorServer/Models/BusinessModel.cs | 5 ++--- .../Pages/Business/BusinessCreate.razor | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs index 4a6c2d45..7731527a 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs @@ -4,10 +4,9 @@ namespace Goodtocode.Subjects.BlazorServer.Models; public class BusinessModel : IBusinessEntity -{ - [Required] - public string Name { get; set; } = string.Empty; +{ public Guid BusinessKey { get; set; } = default; + [Required] public string BusinessName { get; set; } = string.Empty; public string TaxNumber { get; set; } = string.Empty; public string Type { get; set; } = string.Empty; diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor index f33db7c2..8624ec52 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor @@ -16,7 +16,7 @@
-
@@ -24,7 +24,7 @@
- + From 65000437247ecd464ccdfd373f048a34ae9173a8 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Mon, 3 Jul 2023 21:49:42 -0700 Subject: [PATCH 12/25] Added secret connection --- .../Properties/launchSettings.json | 41 ++++++++++++ .../appsettings.Local.json | 25 +++++++ .../appsettings.Production.json | 2 +- .../Presentation.Web.BlazorServer.csproj | 1 + .../local/appInsights1.arm.json | 67 +++++++++++++++++++ .../Properties/launchSettings.json | 28 ++++++++ .../Properties/serviceDependencies.json | 5 ++ .../Properties/serviceDependencies.local.json | 7 ++ .../appsettings.Production.json | 36 ++++++++++ 9 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 src/Subjects/Presentation.Api.WebApi/Properties/launchSettings.json create mode 100644 src/Subjects/Presentation.Api.WebApi/appsettings.Local.json create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Properties/ServiceDependencies/local/appInsights1.arm.json create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Properties/launchSettings.json create mode 100644 src/Subjects/Presentation.Web.BlazorServer/appsettings.Production.json diff --git a/src/Subjects/Presentation.Api.WebApi/Properties/launchSettings.json b/src/Subjects/Presentation.Api.WebApi/Properties/launchSettings.json new file mode 100644 index 00000000..069aa16e --- /dev/null +++ b/src/Subjects/Presentation.Api.WebApi/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:42007", + "sslPort": 44323 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5124", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7171;http://localhost:5124", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Subjects/Presentation.Api.WebApi/appsettings.Local.json b/src/Subjects/Presentation.Api.WebApi/appsettings.Local.json new file mode 100644 index 00000000..3c318ff8 --- /dev/null +++ b/src/Subjects/Presentation.Api.WebApi/appsettings.Local.json @@ -0,0 +1,25 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "ClientId": "7cf20ddf-bc1b-423e-b516-43d386b31da5", + "Domain": "GoodToCode.com", + "TenantId": "ad6529dd-8db1-4015-a53d-6ae395fc7e39" + }, + "Azure": { + "UseKeyVault": true, + "KeyVaultUri": "https://kv-subjects-dev-001.vault.azure.net/" + }, + "UseInMemoryDatabase": false + //"ConnectionStrings": { + // "SubjectsConnection": "FROM_APP_SERVICE" + //}, + //"ApplicationInsights": { + // "ConnectionString": "FROM_APP_SERVICE" + //} +} diff --git a/src/Subjects/Presentation.Api.WebApi/appsettings.Production.json b/src/Subjects/Presentation.Api.WebApi/appsettings.Production.json index be04468e..cf739833 100644 --- a/src/Subjects/Presentation.Api.WebApi/appsettings.Production.json +++ b/src/Subjects/Presentation.Api.WebApi/appsettings.Production.json @@ -7,7 +7,7 @@ }, "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "ClientId": "7cf20ddf-bc1b-423e-b516-43d386b31da5", + "ClientId": "CLIENT_ID", "Domain": "GoodToCode.com", "TenantId": "ad6529dd-8db1-4015-a53d-6ae395fc7e39" }, diff --git a/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj b/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj index 97cae788..37854349 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj +++ b/src/Subjects/Presentation.Web.BlazorServer/Presentation.Web.BlazorServer.csproj @@ -9,6 +9,7 @@ enable aspnet-Goodtocode.Subjects.BlazorServer-162fe164-2aae-4a7b-adcf-a4ef2cd6803d 0 + /subscriptions/2d60a88f-6c6a-48e6-844b-69bb857dd4fe/resourceGroups/gtc-rg-subjects-dev-001/providers/microsoft.insights/components/appi-subjects-dev-001 diff --git a/src/Subjects/Presentation.Web.BlazorServer/Properties/ServiceDependencies/local/appInsights1.arm.json b/src/Subjects/Presentation.Web.BlazorServer/Properties/ServiceDependencies/local/appInsights1.arm.json new file mode 100644 index 00000000..a3954418 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Properties/ServiceDependencies/local/appInsights1.arm.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "gtc-rg-subjects-dev-001", + "metadata": { + "_parameterType": "resourceGroup", + "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "westus3", + "metadata": { + "_parameterType": "location", + "description": "Location of the resource group. Resource groups could have different location than resources." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "_parameterType": "location", + "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('appi-subjects-dev-001', subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "name": "appi-subjects-dev-001", + "type": "microsoft.insights/components", + "location": "[parameters('resourceLocation')]", + "kind": "web", + "properties": {}, + "apiVersion": "2015-05-01" + } + ] + } + } + } + ], + "metadata": { + "_dependencyType": "appInsights.azure" + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Properties/launchSettings.json b/src/Subjects/Presentation.Web.BlazorServer/Properties/launchSettings.json new file mode 100644 index 00000000..528517bf --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Properties/launchSettings.json @@ -0,0 +1,28 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:15622", + "sslPort": 44375 + } + }, + "profiles": { + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "applicationUrl": "https://localhost:7169;http://localhost:5127", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Local" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Local" + } + } + } +} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.json b/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.json index 44cc45e7..cd9f6dd7 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.json +++ b/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.json @@ -3,6 +3,11 @@ "identityapp1": { "type": "identityapp", "dynamicId": null + }, + "appInsights1": { + "type": "appInsights", + "connectionId": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "dynamicId": null } } } \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.local.json b/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.local.json index 3c852246..351c7bfb 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.local.json +++ b/src/Subjects/Presentation.Web.BlazorServer/Properties/serviceDependencies.local.json @@ -3,6 +3,13 @@ "identityapp1": { "type": "identityapp.default", "dynamicId": null + }, + "appInsights1": { + "secretStore": "LocalSecretsFile", + "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/microsoft.insights/components/appi-subjects-dev-001", + "type": "appInsights.azure", + "connectionId": "APPLICATIONINSIGHTS_CONNECTION_STRING", + "dynamicId": null } } } \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/appsettings.Production.json b/src/Subjects/Presentation.Web.BlazorServer/appsettings.Production.json new file mode 100644 index 00000000..7566787a --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/appsettings.Production.json @@ -0,0 +1,36 @@ +{ + "DetailedErrors": true, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "AzureAd": { + "Instance": "https://GoodToCodeB2C.b2clogin.com/", + "Domain": "GoodToCodeB2C.onmicrosoft.com", + "TenantId": "TENANT_ID", + "ClientId": "CLIENT_ID", + "CallbackPath": "/signin-oidc", + "SignUpSignInPolicyId": "B2C_1_Signup_Signin", + "SignedOutCallbackPath": "/signout/B2C_1_susi", + "ResetPasswordPolicyId": "b2c_1_reset", + "EditProfilePolicyId": "b2c_1_edit_profile", + "EnablePiiLogging": true + }, + "Azure": { + "UseKeyVault": true, + "KeyVaultUri": "https://kv-subjects-prod-001.vault.azure.net/" + }, + "Subjects": { + "ClientId": "CLIENT_ID", + "TokenUrl": "https://login.microsoftonline.com/ad6529dd-8db1-4015-a53d-6ae395fc7e39/oauth2/v2.0/token", + "Scope": "api://API_CLIENT_ID/.default", + "Url": "https://api-subjects-prod-001.azurewebsites.net" + } + //"Subjects:ClientSecret": "FROM_KEY_VAULT" + //"ApplicationInsights": { + // "ConnectionString": "FROM_APP_SERVICE" + //} +} From 05b7c1bd4425409b94436424bc3ec4cc13c66e28 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Tue, 4 Jul 2023 10:51:25 -0700 Subject: [PATCH 13/25] Create working --- .../Paging/Pager.razor | 1 + .../Pages/Business/BusinessCreate.razor | 4 +- .../Pages/Business/BusinessList.razor | 19 ++- .../Pages/Business/BusinessSearch.razor | 148 ++++++++++++++++++ .../Services/BusinessService.cs | 26 ++- .../Shared/NavMenu.razor | 10 +- 6 files changed, 194 insertions(+), 14 deletions(-) create mode 100644 src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessSearch.razor diff --git a/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor index bf44a977..a4ffe57e 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor @@ -1,4 +1,5 @@ @using Goodtocode.Common.Extensions; + @if (Result != null) {
diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor index 8624ec52..17fca04d 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor @@ -1,4 +1,4 @@ -@page "/businesscreate" +@page "/business/create" @using Goodtocode.Subjects.BlazorServer.Data; @using Goodtocode.Subjects.BlazorServer.Models; @using Goodtocode.Subjects.Domain; @@ -31,7 +31,7 @@ @code { private BusinessModel business = new BusinessModel(); private string alertMessage = string.Empty; - private CancellationTokenSource cts = new CancellationTokenSource(); + private CancellationTokenSource? cts; private async Task CreateBusineses() { if (!Validator.TryValidateObject(business, diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor index 82fae643..9c12785a 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -1,10 +1,12 @@ -@page "/businesssearch" +@page "/business" +@page "/business/{page:int}" @using Goodtocode.Common.Extensions; @using Goodtocode.Subjects.BlazorServer.Data; @using Goodtocode.Subjects.BlazorServer.Models; @using Goodtocode.Subjects.Domain; @using System.ComponentModel.DataAnnotations; @using Goodtocode.Subjects.Rcl; +@using Goodtocode.Subjects.Rcl.Paging @using Microsoft.AspNetCore.Http.Extensions; @inject BusinessService Service @@ -47,8 +49,8 @@
@business.BusinessName @business.TaxNumber - - + +
+ } @code { [Parameter] - public int Page { get; set; } = 1; + public int page { get; set; } = 1; [Parameter] public string SearchTerm { get; set; } = string.Empty; private string alertMessage = string.Empty; @@ -83,6 +86,12 @@ PageHistory.AddPageToHistory(UriHelper.Uri); } + protected void PagerPageChanged(int page) + { + UriHelper.NavigateTo("/businesslist/" + page); + PageHistory.AddPageToHistory(UriHelper.Uri); + } + protected async Task SearchBoxKeyPress(KeyboardEventArgs ev) { if (ev.Key == "Enter") @@ -106,7 +115,7 @@ { processing = true; await Task.Delay(500, cts.Token); - businesses = await Service.GetBusinessesAsync(businessSearch.Name, Page); + businesses = await Service.GetBusinessesAsync(page); if (businesses.Results.Count() == 0) alertMessage = "No businesses found"; } diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessSearch.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessSearch.razor new file mode 100644 index 00000000..799456e6 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessSearch.razor @@ -0,0 +1,148 @@ +@page "/business/search" +@using Goodtocode.Common.Extensions; +@using Goodtocode.Subjects.BlazorServer.Data; +@using Goodtocode.Subjects.BlazorServer.Models; +@using Goodtocode.Subjects.Domain; +@using System.ComponentModel.DataAnnotations; +@using Goodtocode.Subjects.Rcl; +@using Microsoft.AspNetCore.Http.Extensions; + +@inject BusinessService Service +@inject PageHistoryState PageHistory +@inject NavigationManager UriHelper + +Business Search + + + +
+ + +
+ + + +
+ + +@if (businesses.Results.Count() > 0) +{ + + + + + + + + + + + @foreach (var business in businesses.Results) + { + + + + + + + } + +
KeyNameTax Number
@business.BusinessKey@business.BusinessName@business.TaxNumber + + + +
+} + +@code { + [Parameter] + public int Page { get; set; } = 1; + [Parameter] + public string SearchTerm { get; set; } = string.Empty; + private string alertMessage = string.Empty; + private SearchModel businessSearch = new SearchModel(); + private PagedResult businesses = new PagedResult(); + private CancellationTokenSource cts = new CancellationTokenSource(); + private bool processing; + + protected override void OnInitialized() + { + PageHistory.AddPageToHistory(UriHelper.Uri); + base.OnInitialized(); + } + + protected override async Task OnParametersSetAsync() + { + await GetBusinessesAsync(); + PageHistory.AddPageToHistory(UriHelper.Uri); + } + + protected async Task SearchBoxKeyPress(KeyboardEventArgs ev) + { + if (ev.Key == "Enter") + { + await GetBusinessesAsync(); + } + } + + private async Task GetBusinessesAsync() + { + alertMessage = string.Empty; + + businessSearch.Name = SearchTerm; + + if (!Validator.TryValidateObject(businessSearch, + new ValidationContext(businessSearch, serviceProvider: null, items: null), new List(), true)) return; + + if (cts != null) cts.Cancel(); + cts = new CancellationTokenSource(); + try + { + processing = true; + await Task.Delay(500, cts.Token); + businesses = await Service.GetBusinessesAsync(businessSearch.Name, Page); + if (businesses.Results.Count() == 0) + alertMessage = "No businesses found"; + } + catch (TaskCanceledException) + { + // Ignore exception if task was cancelled + } + finally + { + processing = false; + StateHasChanged(); + } + } + + private async Task DeleteBusinessAsync(BusinessModel business) + { + alertMessage = string.Empty; + + if (cts != null) cts.Cancel(); + cts = new CancellationTokenSource(); + try + { + processing = true; + var businessToDelete = business; + await Task.Delay(500, cts.Token); + await Service.DeleteBusinessAsync(business.BusinessKey); + await GetBusinessesAsync(); + } + catch (TaskCanceledException) + { + // Ignore exception if task was cancelled + } + finally + { + processing = false; + StateHasChanged(); + } + } +} diff --git a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs index 5d7dcd36..d3042b5d 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Services/BusinessService.cs @@ -20,7 +20,7 @@ public BusinessService(IHttpClientFactory clientFactory) public async Task GetBusinessAsync(Guid businessKey) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.GetAsync($"{httpClient.BaseAddress}/Business?key={businessKey}&api-version={apiVersion}"); + var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Business?key={businessKey}&api-version={apiVersion}"); var business = new BusinessModel(); if (response.StatusCode != HttpStatusCode.NotFound) { @@ -32,11 +32,25 @@ public async Task GetBusinessAsync(Guid businessKey) return business; } + public async Task> GetBusinessesAsync(int page) + { + var business = new PagedResult(); + var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); + var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Businesses?pageNumber={page}&pageSize=20&api-version={apiVersion}"); + if (response.StatusCode != HttpStatusCode.NotFound) + { + response.EnsureSuccessStatusCode(); + business = JsonSerializer.Deserialize>(response.Content.ReadAsStream()) ?? throw new Exception("Deserialization failed."); + } + + return business; + } + public async Task> GetBusinessesAsync(string name, int page) { var business = new PagedResult(); var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Businesses?name={name}&pageNumber=1&pageSize=20&api-version={apiVersion}"); + var response = await httpClient.GetAsync($"{httpClient.BaseAddress}Businesses?name={name}&pageNumber={page}&pageSize=20&api-version={apiVersion}"); if (response.StatusCode != HttpStatusCode.NotFound) { response.EnsureSuccessStatusCode(); @@ -49,21 +63,23 @@ public async Task> GetBusinessesAsync(string name, in public async Task CreateBusinessAsync(BusinessModel business) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.PutAsJsonAsync($"{httpClient.BaseAddress}/Business?api-version=1", business.CopyPropertiesSafe()); + var businessDto = business.CopyPropertiesSafe(); + var response = await httpClient.PutAsJsonAsync($"{httpClient.BaseAddress}Business?api-version=1", businessDto); response.EnsureSuccessStatusCode(); } public async Task UpdateBusinessAsync(BusinessModel business) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.PostAsJsonAsync($"{httpClient.BaseAddress}/Business?key={business.BusinessKey}api-version={apiVersion}", business.CopyPropertiesSafe()); + var businessDto = business.CopyPropertiesSafe(); + var response = await httpClient.PostAsJsonAsync($"{httpClient.BaseAddress}Business?key={business.BusinessKey}api-version={apiVersion}", businessDto); response.EnsureSuccessStatusCode(); } public async Task DeleteBusinessAsync(Guid businessKey) { var httpClient = _clientFactory.CreateClient("SubjectsApiClient"); - var response = await httpClient.DeleteAsync($"{httpClient.BaseAddress}/Business?key={businessKey}api-version={apiVersion}"); + var response = await httpClient.DeleteAsync($"{httpClient.BaseAddress}Business?key={businessKey}api-version={apiVersion}"); response.EnsureSuccessStatusCode(); } } \ No newline at end of file diff --git a/src/Subjects/Presentation.Web.BlazorServer/Shared/NavMenu.razor b/src/Subjects/Presentation.Web.BlazorServer/Shared/NavMenu.razor index e28fca0d..c1d75e25 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Shared/NavMenu.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Shared/NavMenu.razor @@ -15,12 +15,18 @@ + + From 96dd10d5eebd9baff4ba9a506bf8d6104a325c4d Mon Sep 17 00:00:00 2001 From: Robert Good Date: Wed, 5 Jul 2023 09:30:15 -0700 Subject: [PATCH 14/25] Simple alert created --- .../Presentation.Shared.Rcl/Alert/Alert.razor | 117 ++++++++++++++++++ .../Alert/AlertModel.cs | 19 +++ .../Alert/AlertService.cs | 73 +++++++++++ .../Alert/SimpleAlert.razor | 12 ++ .../Alert/SimpleAlert.razor.css | 0 .../Pages/Business/BusinessCreate.razor | 3 +- .../Presentation.Web.BlazorServer/Program.cs | 3 +- 7 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor create mode 100644 src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs create mode 100644 src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor create mode 100644 src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor.css diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor b/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor new file mode 100644 index 00000000..f0747e0d --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor @@ -0,0 +1,117 @@ +@using Microsoft.AspNetCore.Components.Routing; +@implements IDisposable +@inject IAlertService AlertService +@inject NavigationManager NavigationManager + +@foreach (var alert in alerts) +{ +
+ × + @alert.Message +
+} + +@code { + [Parameter] + public string Id { get; set; } = "default-alert"; + + [Parameter] + public bool Fade { get; set; } = true; + + private List alerts = new List(); + + protected override void OnInitialized() + { + // subscribe to new alerts and location change events + AlertService.OnAlert += OnAlert; + NavigationManager.LocationChanged += OnLocationChange; + } + + public void Dispose() + { + // unsubscribe from alerts and location change events + AlertService.OnAlert -= OnAlert; + NavigationManager.LocationChanged -= OnLocationChange; + } + + private async void OnAlert(AlertModel alert) + { + // ignore alerts sent to other alert components + if (alert.Id != Id) + return; + + // clear alerts when an empty alert is received + if (alert.Message == null) + { + // remove alerts without the 'KeepAfterRouteChange' flag set to true + alerts.RemoveAll(x => !x.KeepAfterRouteChange); + + // set the 'KeepAfterRouteChange' flag to false for the + // remaining alerts so they are removed on the next clear + alerts.ForEach(x => x.KeepAfterRouteChange = false); + } + else + { + // add alert to array + alerts.Add(alert); + StateHasChanged(); + + // auto close alert if required + if (alert.AutoClose) + { + await Task.Delay(3000); + RemoveAlert(alert); + } + } + + StateHasChanged(); + } + + private void OnLocationChange(object sender, LocationChangedEventArgs e) + { + AlertService.Clear(Id); + } + + private async void RemoveAlert(AlertModel alert) + { + // check if already removed to prevent error on auto close + if (!alerts.Contains(alert)) return; + + if (Fade) + { + // fade out alert + alert.Fade = true; + + // remove alert after faded out + await Task.Delay(250); + alerts.Remove(alert); + } + else + { + // remove alert + alerts.Remove(alert); + } + + StateHasChanged(); + } + + private string CssClass(AlertModel alert) + { + if (alert == null) return null; + + var classes = new List { "alert", "alert-dismissable", "mt-4", "container" }; + + var alertTypeClass = new Dictionary(); + alertTypeClass[AlertType.Success] = "alert-success"; + alertTypeClass[AlertType.Error] = "alert-danger"; + alertTypeClass[AlertType.Info] = "alert-info"; + alertTypeClass[AlertType.Warning] = "alert-warning"; + + classes.Add(alertTypeClass[alert.Type]); + + if (alert.Fade) + classes.Add("fade"); + + return string.Join(' ', classes); + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs new file mode 100644 index 00000000..c8128ad4 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs @@ -0,0 +1,19 @@ +namespace Goodtocode.Subjects.Rcl; + +public class AlertModel +{ + public string Id { get; set; } + public AlertType Type { get; set; } + public string Message { get; set; } + public bool AutoClose { get; set; } + public bool KeepAfterRouteChange { get; set; } + public bool Fade { get; set; } +} + +public enum AlertType +{ + Success, + Error, + Info, + Warning +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs new file mode 100644 index 00000000..bce523f6 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs @@ -0,0 +1,73 @@ +namespace Goodtocode.Subjects.Rcl; + +public interface IAlertService +{ + event Action OnAlert; + void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true); + void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true); + void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true); + void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true); + void Alert(AlertModel alert); + void Clear(string id = null); +} + +public class AlertService : IAlertService +{ + private const string _defaultId = "default-alert"; + public event Action OnAlert; + + public void Success(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertType.Success, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertType.Error, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertType.Info, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertType.Warning, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Alert(AlertModel alert) + { + alert.Id = alert.Id ?? _defaultId; + this.OnAlert?.Invoke(alert); + } + + public void Clear(string id = _defaultId) + { + this.OnAlert?.Invoke(new AlertModel { Id = id }); + } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor b/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor new file mode 100644 index 00000000..88ccb943 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor @@ -0,0 +1,12 @@ +@using Goodtocode.Common.Extensions; + + + +@code { + [Parameter] + public string Message { get; set; } + + protected override async Task OnParametersSetAsync() + { + } +} diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor.css b/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor.css new file mode 100644 index 00000000..e69de29b diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor index 17fca04d..45160d2b 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor @@ -3,6 +3,7 @@ @using Goodtocode.Subjects.BlazorServer.Models; @using Goodtocode.Subjects.Domain; @using System.ComponentModel.DataAnnotations; +@using Goodtocode.Subjects.Rcl.Alert @inject BusinessService Service @@ -25,7 +26,7 @@ - +
@code { diff --git a/src/Subjects/Presentation.Web.BlazorServer/Program.cs b/src/Subjects/Presentation.Web.BlazorServer/Program.cs index af0f9ae7..bb5dcbfd 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Program.cs +++ b/src/Subjects/Presentation.Web.BlazorServer/Program.cs @@ -50,8 +50,9 @@ builder.Configuration["Subjects:Scope"]) ); -builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddScoped(); +builder.Services.AddSingleton(); var app = builder.Build(); From 4eb6b9790a25a9c8fcd8376a6b2ea796789900e7 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Fri, 7 Jul 2023 17:56:13 -0700 Subject: [PATCH 15/25] Added colors to simple alert --- .../Presentation.Shared.Rcl/Alert/Alert.razor | 10 ++++----- .../Alert/AlertModel.cs | 10 +-------- .../Alert/AlertService.cs | 8 +++---- .../Alert/AlertTypes.cs | 9 ++++++++ .../Alert/SimpleAlert.razor | 21 ++++++++++++++++++- .../Pages/Business/BusinessCreate.razor | 5 ++++- 6 files changed, 43 insertions(+), 20 deletions(-) create mode 100644 src/Subjects/Presentation.Shared.Rcl/Alert/AlertTypes.cs diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor b/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor index f0747e0d..1d97554f 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/Alert.razor @@ -101,11 +101,11 @@ var classes = new List { "alert", "alert-dismissable", "mt-4", "container" }; - var alertTypeClass = new Dictionary(); - alertTypeClass[AlertType.Success] = "alert-success"; - alertTypeClass[AlertType.Error] = "alert-danger"; - alertTypeClass[AlertType.Info] = "alert-info"; - alertTypeClass[AlertType.Warning] = "alert-warning"; + var alertTypeClass = new Dictionary(); + alertTypeClass[AlertTypes.Success] = "alert-success"; + alertTypeClass[AlertTypes.Error] = "alert-danger"; + alertTypeClass[AlertTypes.Info] = "alert-info"; + alertTypeClass[AlertTypes.Warning] = "alert-warning"; classes.Add(alertTypeClass[alert.Type]); diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs index c8128ad4..c584a5f0 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertModel.cs @@ -3,17 +3,9 @@ namespace Goodtocode.Subjects.Rcl; public class AlertModel { public string Id { get; set; } - public AlertType Type { get; set; } + public AlertTypes Type { get; set; } public string Message { get; set; } public bool AutoClose { get; set; } public bool KeepAfterRouteChange { get; set; } public bool Fade { get; set; } -} - -public enum AlertType -{ - Success, - Error, - Info, - Warning } \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs index bce523f6..132fd2d5 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertService.cs @@ -20,7 +20,7 @@ public void Success(string message, bool keepAfterRouteChange = false, bool auto { this.Alert(new AlertModel { - Type = AlertType.Success, + Type = AlertTypes.Success, Message = message, KeepAfterRouteChange = keepAfterRouteChange, AutoClose = autoClose @@ -31,7 +31,7 @@ public void Error(string message, bool keepAfterRouteChange = false, bool autoCl { this.Alert(new AlertModel { - Type = AlertType.Error, + Type = AlertTypes.Error, Message = message, KeepAfterRouteChange = keepAfterRouteChange, AutoClose = autoClose @@ -42,7 +42,7 @@ public void Info(string message, bool keepAfterRouteChange = false, bool autoClo { this.Alert(new AlertModel { - Type = AlertType.Info, + Type = AlertTypes.Info, Message = message, KeepAfterRouteChange = keepAfterRouteChange, AutoClose = autoClose @@ -53,7 +53,7 @@ public void Warn(string message, bool keepAfterRouteChange = false, bool autoClo { this.Alert(new AlertModel { - Type = AlertType.Warning, + Type = AlertTypes.Warning, Message = message, KeepAfterRouteChange = keepAfterRouteChange, AutoClose = autoClose diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/AlertTypes.cs b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertTypes.cs new file mode 100644 index 00000000..4e98ed3b --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/AlertTypes.cs @@ -0,0 +1,9 @@ +namespace Goodtocode.Subjects.Rcl; + +public enum AlertTypes +{ + Success, + Error, + Info, + Warning +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor b/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor index 88ccb943..edfed93b 100644 --- a/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor +++ b/src/Subjects/Presentation.Shared.Rcl/Alert/SimpleAlert.razor @@ -1,12 +1,31 @@ @using Goodtocode.Common.Extensions; - + @code { [Parameter] public string Message { get; set; } + [Parameter] + public AlertTypes AlertType { get; set; } = AlertTypes.Error; + + private string alertClass = "alert-danger"; protected override async Task OnParametersSetAsync() { + switch(AlertType) + { + case AlertTypes.Error: + alertClass = "alert-danger"; + break; + case AlertTypes.Warning: + alertClass = "alert-warning"; + break; + case AlertTypes.Info: + alertClass = "alert-info"; + break; + case AlertTypes.Success: + alertClass = "alert-success"; + break; + } } } diff --git a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor index 45160d2b..96f517cf 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor @@ -4,6 +4,7 @@ @using Goodtocode.Subjects.Domain; @using System.ComponentModel.DataAnnotations; @using Goodtocode.Subjects.Rcl.Alert +@using Goodtocode.Subjects.Rcl; @inject BusinessService Service @@ -26,12 +27,13 @@ - + @code { private BusinessModel business = new BusinessModel(); private string alertMessage = string.Empty; + private AlertTypes alertType = AlertTypes.Error; private CancellationTokenSource? cts; private async Task CreateBusineses() { @@ -46,6 +48,7 @@ alertMessage = string.Empty; await Service.CreateBusinessAsync(business); alertMessage = $"{business.BusinessName} created"; + alertType = AlertTypes.Success; } catch (TaskCanceledException) { From 34f5e8d550b8a7660723fd0de316d0668b30fbd5 Mon Sep 17 00:00:00 2001 From: Robert Good Date: Sun, 9 Jul 2023 09:40:53 -0700 Subject: [PATCH 16/25] Added GetAll --- ...inessQuery.cs => GetBusinessByKeyQuery.cs} | 12 +++---- ...r.cs => GetBusinessbyKeyQueryValidator.cs} | 4 +-- .../Business/Queries/GetBusinessesAllQuery.cs | 32 +++++++++++++++++++ .../Queries/GetBusinessesAllQueryValidator.cs | 13 ++++++++ .../Queries/GetBusinessesByNameQuery.cs | 3 +- .../GetBusinessesByNameQueryValidator.cs | 2 ++ .../Interfaces/IBusinessRepo.cs | 7 ++-- .../Repositories/BusinessRepo.cs | 18 +++++++---- .../Controllers/BusinessController.cs | 2 +- .../Controllers/BusinessesController.cs | 17 +++++++++- .../Goodtocode.Subjects.WebApi.xml | 9 ++++++ .../GetBusinessesByKeyStepDefinitions.cs | 6 ++-- .../GetBusinessesByKeyStepDefinitions.cs | 8 ++--- .../GetBusinessesByNameStepDefinitions.cs | 2 +- 14 files changed, 107 insertions(+), 28 deletions(-) rename src/Subjects/Core.Application/Business/Queries/{GetBusinessQuery.cs => GetBusinessByKeyQuery.cs} (57%) rename src/Subjects/Core.Application/Business/Queries/{GetBusinessQueryValidator.cs => GetBusinessbyKeyQueryValidator.cs} (50%) create mode 100644 src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQuery.cs create mode 100644 src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQueryValidator.cs diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessQuery.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessByKeyQuery.cs similarity index 57% rename from src/Subjects/Core.Application/Business/Queries/GetBusinessQuery.cs rename to src/Subjects/Core.Application/Business/Queries/GetBusinessByKeyQuery.cs index 1ff050be..7dbb9b86 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessQuery.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessByKeyQuery.cs @@ -6,29 +6,27 @@ namespace Goodtocode.Subjects.Application; -public class GetBusinessQuery : IRequest, IBusinessEntity +public class GetBusinessByKeyQuery : IRequest { public Guid BusinessKey { get; set; } - public string BusinessName { get; set; } = string.Empty; - public string TaxNumber { get; set; } = string.Empty; } -public class GetBusinessQueryHandler : IRequestHandler +public class GetBusinessByKeyQueryHandler : IRequestHandler { private readonly IMapper _mapper; private readonly IBusinessRepo _userBusinessRepo; - public GetBusinessQueryHandler(IBusinessRepo userBusinessRepo, IMapper mapper) + public GetBusinessByKeyQueryHandler(IBusinessRepo userBusinessRepo, IMapper mapper) { _userBusinessRepo = userBusinessRepo; _mapper = mapper; } - public async Task Handle(GetBusinessQuery request, + public async Task Handle(GetBusinessByKeyQuery request, CancellationToken cancellationToken) { var business = - await _userBusinessRepo.GetBusinessAsync(request.BusinessKey, + await _userBusinessRepo.GetBusinessByKeyAsync(request.BusinessKey, cancellationToken); return business.Match( diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessQueryValidator.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessbyKeyQueryValidator.cs similarity index 50% rename from src/Subjects/Core.Application/Business/Queries/GetBusinessQueryValidator.cs rename to src/Subjects/Core.Application/Business/Queries/GetBusinessbyKeyQueryValidator.cs index 49ef160c..db9fc41d 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessQueryValidator.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessbyKeyQueryValidator.cs @@ -2,9 +2,9 @@ namespace Goodtocode.Subjects.Application; -public class GetBusinessQueryValidator : AbstractValidator +public class GetBusinessbyKeyQueryValidator : AbstractValidator { - public GetBusinessQueryValidator() + public GetBusinessbyKeyQueryValidator() { RuleFor(x => x.BusinessKey).NotEmpty(); } diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQuery.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQuery.cs new file mode 100644 index 00000000..fef64c8a --- /dev/null +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQuery.cs @@ -0,0 +1,32 @@ +using Goodtocode.Common.Extensions; +using Goodtocode.Subjects.Domain; +using MediatR; + +namespace Goodtocode.Subjects.Application; + +public class GetBusinessesAllQuery : IRequest> +{ + public string BusinessName { get; set; } = string.Empty; + public int Page { get; set; } = 1; + public int Results { get; set; } = 20; +} + +public class GetBusinessesAllQueryHandler : IRequestHandler> +{ + private readonly IBusinessRepo _userBusinessesRepo; + + public GetBusinessesAllQueryHandler(IBusinessRepo userBusinessesRepo) + { + _userBusinessesRepo = userBusinessesRepo; + } + + public async Task> Handle(GetBusinessesAllQuery request, + CancellationToken cancellationToken) + { + var businesses = + await _userBusinessesRepo.GetBusinessesAllAsync(request.BusinessName, request.Page, request.Results, + cancellationToken); + + return businesses.GetValueOrDefault(); + } +} \ No newline at end of file diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQueryValidator.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQueryValidator.cs new file mode 100644 index 00000000..5f8863e2 --- /dev/null +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessesAllQueryValidator.cs @@ -0,0 +1,13 @@ +using FluentValidation; + +namespace Goodtocode.Subjects.Application; + +public class GetBusinessesAllQueryValidator : AbstractValidator +{ + public GetBusinessesAllQueryValidator() + { + RuleFor(x => x.BusinessName).NotEmpty(); + RuleFor(x => x.Results).GreaterThan(0); + RuleFor(x => x.Page).GreaterThan(0); + } +} \ No newline at end of file diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs index 5caa3548..3569d00f 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs @@ -8,6 +8,7 @@ public class GetBusinessesByNameQuery : IRequest> { public string BusinessName { get; set; } = string.Empty; public int Page { get; set; } = 1; + public int Results { get; set; } = 20; } public class GetBusinessesByNameQueryHandler : IRequestHandler> @@ -23,7 +24,7 @@ public async Task> Handle(GetBusinessesByNameQuery r CancellationToken cancellationToken) { var businesses = - await _userBusinessesRepo.GetBusinessesByNameAsync(request.BusinessName, request.Page, + await _userBusinessesRepo.GetBusinessesByNameAsync(request.BusinessName, request.Page, request.Results, cancellationToken); return businesses.GetValueOrDefault(); diff --git a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQueryValidator.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQueryValidator.cs index 69a0e56a..1ea4f5b6 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQueryValidator.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQueryValidator.cs @@ -7,5 +7,7 @@ public class GetBusinessesByNameQueryValidator : AbstractValidator x.BusinessName).NotEmpty(); + RuleFor(x => x.Results).GreaterThan(0); + RuleFor(x => x.Page).GreaterThan(0); } } \ No newline at end of file diff --git a/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs b/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs index ce797664..f606bc9c 100644 --- a/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs +++ b/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs @@ -6,10 +6,13 @@ namespace Goodtocode.Subjects.Application; public interface IBusinessRepo { - Task> GetBusinessAsync(Guid businessKey, + Task> GetBusinessByKeyAsync(Guid businessKey, CancellationToken cancellationToken); - Task>> GetBusinessesByNameAsync(string businessName, int page, + Task>> GetBusinessesAllAsync(string businessName, int page, int results, + CancellationToken cancellationToken); + + Task>> GetBusinessesByNameAsync(string businessName, int page, int results, CancellationToken cancellationToken); Task> AddBusinessAsync(IBusinessObject businessInfo, diff --git a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs index 956595de..355cd49c 100644 --- a/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs +++ b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs @@ -11,15 +11,13 @@ namespace Goodtocode.Subjects.Persistence.Repositories; public class BusinessRepo : IBusinessRepo { private readonly ISubjectsDbContext _context; - private readonly int _pageSize = 20; - public BusinessRepo(ISubjectsDbContext context, int pageSize = 20) + public BusinessRepo(ISubjectsDbContext context) { _context = context; - _pageSize = pageSize; } - public async Task> GetBusinessAsync(Guid businessKey, CancellationToken cancellationToken) + public async Task> GetBusinessByKeyAsync(Guid businessKey, CancellationToken cancellationToken) { var businessResult = await _context.Business.FindAsync(new object?[] { businessKey, cancellationToken }, cancellationToken: cancellationToken); if (businessResult != null) @@ -28,12 +26,20 @@ public BusinessRepo(ISubjectsDbContext context, int pageSize = 20) return Result.Failure("Business not found."); } - public async Task>> GetBusinessesByNameAsync(string businessName, int page, CancellationToken cancellationToken) + public async Task>> GetBusinessesAllAsync(string businessName, int page, int results, CancellationToken cancellationToken) + { + var businessResult = await _context.Business + .OrderBy(b => b.BusinessKey) + .GetPagedAsync(page, results, cancellationToken); + return Result.Success(businessResult); + } + + public async Task>> GetBusinessesByNameAsync(string businessName, int page, int results, CancellationToken cancellationToken) { var businessResult = await _context.Business .Where(b => b.BusinessName.Contains(businessName)) .OrderBy(b => b.BusinessKey) - .GetPagedAsync(page, _pageSize, cancellationToken); + .GetPagedAsync(page, results, cancellationToken); return Result.Success(businessResult); } diff --git a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs index 148ce190..e975eab7 100644 --- a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs +++ b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessController.cs @@ -27,7 +27,7 @@ public class BusinessController : BaseController [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task Get(Guid key) { - return await Mediator.Send(new GetBusinessQuery + return await Mediator.Send(new GetBusinessByKeyQuery { BusinessKey = key }); diff --git a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs index 7d564e80..55f42bbc 100644 --- a/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs +++ b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs @@ -27,6 +27,21 @@ public class BusinessesController : BaseController [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> Get(string name, int pageNumber = 1, int pageSize = 20) => await Mediator.Send(new GetBusinessesByNameQuery { - BusinessName = name + BusinessName = name, + Page = pageNumber, + }); + + /// Get Businesses by Name + /// + /// Sample request: + /// "businessName": "My Business" + /// "api-version": 1 + /// + /// Collection of BusinessEntity + [HttpGet(Name = "GetBusinessesAllQuery")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Get(int pageNumber = 1, int pageSize = 20) => await Mediator.Send(new GetBusinessesAllQuery + { }); } \ No newline at end of file diff --git a/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml b/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml index a578067f..d6a82150 100644 --- a/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml +++ b/src/Subjects/Presentation.Api.WebApi/Goodtocode.Subjects.WebApi.xml @@ -129,5 +129,14 @@ Collection of BusinessEntity + + Get Businesses by Name + + Sample request: + "businessName": "My Business" + "api-version": 1 + + Collection of BusinessEntity + diff --git a/src/Subjects/Specs.Integration/Business/GetBusinessesByKeyStepDefinitions.cs b/src/Subjects/Specs.Integration/Business/GetBusinessesByKeyStepDefinitions.cs index faffe73f..43a0cb12 100644 --- a/src/Subjects/Specs.Integration/Business/GetBusinessesByKeyStepDefinitions.cs +++ b/src/Subjects/Specs.Integration/Business/GetBusinessesByKeyStepDefinitions.cs @@ -34,19 +34,19 @@ public void GivenIHaveABusinessName(Guid businessKey) [When(@"I query for matching Businesses")] public async Task WhenIQueryForMatchingBusinesses() { - var request = new GetBusinessQuery + var request = new GetBusinessByKeyQuery { BusinessKey = _businessKey }; - var requestValidator = new GetBusinessQueryValidator(); + var requestValidator = new GetBusinessbyKeyQueryValidator(); _validationErrors = await requestValidator.ValidateAsync(request); if (_validationErrors.IsValid) try { - var handler = new GetBusinessQueryHandler(BusinessRepo, Mapper); + var handler = new GetBusinessByKeyQueryHandler(BusinessRepo, Mapper); _response = await handler.Handle(request, CancellationToken.None) ?? new BusinessEntity(); _responseType = CommandResponseType.Successful; } diff --git a/src/Subjects/Specs.Unit/Business/GetBusinessesByKeyStepDefinitions.cs b/src/Subjects/Specs.Unit/Business/GetBusinessesByKeyStepDefinitions.cs index a49926aa..6f9ae0b4 100644 --- a/src/Subjects/Specs.Unit/Business/GetBusinessesByKeyStepDefinitions.cs +++ b/src/Subjects/Specs.Unit/Business/GetBusinessesByKeyStepDefinitions.cs @@ -47,7 +47,7 @@ public async Task WhenIQueryForMatchingBusinesses() if (_businessExists) { userBusinessesRepoMock - .Setup(x => x.GetBusinessAsync(_businessKey, It.IsAny())) + .Setup(x => x.GetBusinessByKeyAsync(_businessKey, It.IsAny())) .Returns(Task.FromResult(Result.Success(new BusinessEntity() { BusinessKey = new Guid("2016a497-e56c-4be8-8ef6-3dc5ae1699ce"), @@ -55,19 +55,19 @@ public async Task WhenIQueryForMatchingBusinesses() }))); } - var request = new GetBusinessQuery + var request = new GetBusinessByKeyQuery { BusinessKey = _businessKey }; - var requestValidator = new GetBusinessQueryValidator(); + var requestValidator = new GetBusinessbyKeyQueryValidator(); _validationErrors = await requestValidator.ValidateAsync(request); if (_validationErrors.IsValid) try { - var handler = new GetBusinessQueryHandler(userBusinessesRepoMock.Object, Mapper); + var handler = new GetBusinessByKeyQueryHandler(userBusinessesRepoMock.Object, Mapper); _response = await handler.Handle(request, CancellationToken.None) ?? new BusinessEntity(); _responseType = CommandResponseType.Successful; } diff --git a/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs b/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs index 29aabcff..92167976 100644 --- a/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs +++ b/src/Subjects/Specs.Unit/Business/GetBusinessesByNameStepDefinitions.cs @@ -48,7 +48,7 @@ public async Task WhenIQueryForMatchingBusinesses() if (_businessExists) { userBusinessesRepoMock - .Setup(x => x.GetBusinessesByNameAsync(_businessName, 1, It.IsAny())) + .Setup(x => x.GetBusinessesByNameAsync(_businessName, 1, 20, It.IsAny())) .Returns(Task.FromResult(Result.Success(new PagedResult( new() { From 4cb064919aa36c2504315dc733bea23300c0b89a Mon Sep 17 00:00:00 2001 From: Robert Good Date: Sun, 9 Jul 2023 11:03:55 -0700 Subject: [PATCH 17/25] Implemented Set, removed tracking for readonly queries --- .../Interfaces/ISubjectsDbContext.cs | 2 +- .../Contexts/SubjectsDbContext.cs | 47 +++++++++---------- .../Repositories/BusinessRepo.cs | 4 +- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs b/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs index 4cfa325d..f3507917 100644 --- a/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs +++ b/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs @@ -5,7 +5,7 @@ namespace Goodtocode.Subjects.Application; public interface ISubjectsDbContext { - DbSet Business { get; set; } + DbSet Business { get; } //DbSet Detail { get; set; } //DbSet DetailType { get; set; } //DbSet Associate { get; set; } diff --git a/src/Subjects/Infrastructure.Persistence/Contexts/SubjectsDbContext.cs b/src/Subjects/Infrastructure.Persistence/Contexts/SubjectsDbContext.cs index 938c89bb..436b7846 100644 --- a/src/Subjects/Infrastructure.Persistence/Contexts/SubjectsDbContext.cs +++ b/src/Subjects/Infrastructure.Persistence/Contexts/SubjectsDbContext.cs @@ -1,7 +1,6 @@ using Goodtocode.Subjects.Application; using Goodtocode.Subjects.Domain; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using System.Reflection; namespace Goodtocode.Subjects.Persistence.Contexts; @@ -12,29 +11,29 @@ public SubjectsDbContext(DbContextOptions options) : base(options) { } - public virtual DbSet Business { get; set; } - //public virtual DbSet Detail { get; set; } - //public virtual DbSet DetailType { get; set; } - //public virtual DbSet Associate { get; set; } - //public virtual DbSet AssociateDetail { get; set; } - //public virtual DbSet AssociateOption { get; set; } - //public virtual DbSet Gender { get; set; } - //public virtual DbSet Government { get; set; } - //public virtual DbSet Item { get; set; } - //public virtual DbSet ItemGroup { get; set; } - //public virtual DbSet ItemType { get; set; } - //public virtual DbSet