diff --git a/.github/workflows/gtc-rg-subjects-landingzone.yml b/.github/workflows/gtc-rg-subjects-landingzone.yml index b3333b55..4126d7e3 100644 --- a/.github/workflows/gtc-rg-subjects-landingzone.yml +++ b/.github/workflows/gtc-rg-subjects-landingzone.yml @@ -21,8 +21,7 @@ on: description: 'Running mode' env: - API_NAME: 'api-subjects-dev-001' - API_IDENTITY: 'identity-subjects-dev-001' + API_NAME: 'api-subjects-dev-001' APPINSIGHTS_NAME: 'appi-subjects-dev-001' ARM_PATH: './.azure' AZURE_RG_ENVIRONMENT: 'Development' @@ -32,6 +31,7 @@ env: PLAN_NAME: 'plan-entities-dev-001' SHARED_RG_NAME: 'gtc-rg-entities-dev-001' STORAGE_NAME: 'stsubjectsdev001' + USER_IDENTITY: 'identity-subjects-dev-001' WEB_NAME: 'web-subjects-dev-001' WORKSPACE_NAME: 'work-entities-dev-001' @@ -92,12 +92,13 @@ jobs: template: ${{ env.ARM_PATH }}/api-apiapp.json parameters: name=${{ env.API_NAME }} planName=${{ env.PLAN_NAME }} planResourceGroupName=${{ env.SHARED_RG_NAME }} appiKey=${{ secrets.APPI_KEY }} appiConnection=${{ secrets.APPI_CONNECTION }} rgEnvironment=${{ env.AZURE_RG_ENVIRONMENT }} - - name: Identity ${{ env.API_NAME }} + - name: Identity ${{ env.USER_IDENTITY }} run: | - az identity create --resource-group ${{ env.AZURE_RG_NAME }} --name ${{ env.API_IDENTITY }} + az identity create --resource-group ${{ env.AZURE_RG_NAME }} --name ${{ env.USER_IDENTITY }} # The following command requires Security Reader - $objectId = az ad sp list --display-name ${{ env.API_IDENTITY }} --query "[?displayName=='${{ env.API_IDENTITY }}'].id" --output tsv + $objectId = az ad sp list --display-name ${{ env.USER_IDENTITY }} --query "[?displayName=='${{ env.USER_IDENTITY }}'].id" --output tsv az webapp identity assign --resource-group ${{ env.AZURE_RG_NAME }} --name ${{ env.API_NAME }} --identities $objectId + az webapp identity assign --resource-group ${{ env.AZURE_RG_NAME }} --name ${{ env.WEB_NAME }} --identities $objectId az keyvault set-policy -n ${{ env.KEYVAULT_NAME }} -g ${{ env.AZURE_RG_NAME }} --object-id $objectId --secret-permissions get shell: pwsh 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/Common.Persistence/Cache/CacheConfiguration.cs b/src/Subjects/Common.Persistence/Cache/CacheConfiguration.cs new file mode 100644 index 00000000..59bf4c57 --- /dev/null +++ b/src/Subjects/Common.Persistence/Cache/CacheConfiguration.cs @@ -0,0 +1,13 @@ +namespace Goodtocode.Common.Persistence.Cache; + +/// +/// "CacheConfiguration": { +/// "AbsoluteExpirationInHours": 1, +/// "SlidingExpirationInMinutes": 30 +/// } +/// +public class CacheConfiguration +{ + public int AbsoluteExpirationInHours { get; set; } + public int SlidingExpirationInMinutes { get; set; } +} diff --git a/src/Subjects/Common.Persistence/Cache/CacheTypes.cs b/src/Subjects/Common.Persistence/Cache/CacheTypes.cs new file mode 100644 index 00000000..0b3e334d --- /dev/null +++ b/src/Subjects/Common.Persistence/Cache/CacheTypes.cs @@ -0,0 +1,7 @@ +namespace Goodtocode.Common.Persistence.Cache; + +public enum CacheTypes +{ + Redis, + Memory +} diff --git a/src/Subjects/Common.Persistence/Cache/ICacheService.cs b/src/Subjects/Common.Persistence/Cache/ICacheService.cs new file mode 100644 index 00000000..9b38bad6 --- /dev/null +++ b/src/Subjects/Common.Persistence/Cache/ICacheService.cs @@ -0,0 +1,8 @@ +namespace Goodtocode.Common.Persistence.Cache; + +public interface ICacheService +{ + bool TryGet(string cacheKey, out T value); + T Set(string cacheKey, T value); + void Remove(string cacheKey); +} diff --git a/src/Subjects/Common.Persistence/Cache/MemoryCacheService.cs b/src/Subjects/Common.Persistence/Cache/MemoryCacheService.cs new file mode 100644 index 00000000..c2e93c7e --- /dev/null +++ b/src/Subjects/Common.Persistence/Cache/MemoryCacheService.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Options; + +namespace Goodtocode.Common.Persistence.Cache; + +public class MemoryCacheService : ICacheService +{ + private readonly IMemoryCache _cache; + private readonly CacheConfiguration _config; + private readonly MemoryCacheEntryOptions _options; + public MemoryCacheService(IMemoryCache memoryCache, IOptions cacheConfig) + { + _cache = memoryCache; + _config = cacheConfig.Value; + if (_config != null) + { + _options = new MemoryCacheEntryOptions + { + AbsoluteExpiration = DateTime.Now.AddHours(_config.AbsoluteExpirationInHours), + Priority = CacheItemPriority.High, + SlidingExpiration = TimeSpan.FromMinutes(_config.SlidingExpirationInMinutes) + }; + } + } + public bool TryGet(string cacheKey, out T value) + { + _cache.TryGetValue(cacheKey, out value); + if (value == null) return false; + else return true; + } + public T Set(string cacheKey, T value) + { + return _cache.Set(cacheKey, value, _options); + } + public void Remove(string cacheKey) + { + _cache.Remove(cacheKey); + } +} \ No newline at end of file diff --git a/src/Subjects/Common.Persistence/Cache/RedisCacheService.cs b/src/Subjects/Common.Persistence/Cache/RedisCacheService.cs new file mode 100644 index 00000000..f714cf1d --- /dev/null +++ b/src/Subjects/Common.Persistence/Cache/RedisCacheService.cs @@ -0,0 +1,43 @@ +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Options; +using System.Text.Json; + +namespace Goodtocode.Common.Persistence.Cache; + +public class RedisCacheService : ICacheService +{ + private readonly IDistributedCache _cache; + private readonly CacheConfiguration _config; + private readonly DistributedCacheEntryOptions _options; + + public RedisCacheService(IDistributedCache cache, IOptions cacheConfig) + { + _cache = cache; + _config = cacheConfig.Value; + if (_config != null) + { + _options = new DistributedCacheEntryOptions + { + AbsoluteExpiration = DateTime.Now.AddHours(_config.AbsoluteExpirationInHours), + SlidingExpiration = TimeSpan.FromMinutes(_config.SlidingExpirationInMinutes) + }; + } + } + + public bool TryGet(string cacheKey, out T value) + { + var serialized = _cache.GetString(cacheKey); + value = JsonSerializer.Deserialize(serialized); + if (value == null) return false; + else return true; + } + public T Set(string cacheKey, T value) + { + _cache.SetString(cacheKey, JsonSerializer.Serialize(value), _options); + return value; + } + public void Remove(string cacheKey) + { + _cache.Remove(cacheKey); + } +} diff --git a/src/Subjects/Common.Persistence/Common.Persistence.csproj b/src/Subjects/Common.Persistence/Common.Persistence.csproj new file mode 100644 index 00000000..30f874a9 --- /dev/null +++ b/src/Subjects/Common.Persistence/Common.Persistence.csproj @@ -0,0 +1,20 @@ + + + + net7.0 + Goodtocode.Common.Persistence + Goodtocode.Common.Persistence + 1.0.0 + net7.0 + enable + enable + + + + + + + + + + diff --git a/src/Subjects/Common.Persistence/ConfigureServices.cs b/src/Subjects/Common.Persistence/ConfigureServices.cs new file mode 100644 index 00000000..68809890 --- /dev/null +++ b/src/Subjects/Common.Persistence/ConfigureServices.cs @@ -0,0 +1,29 @@ +using Goodtocode.Common.Persistence.Cache; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Goodtocode.Common.Persistence; + +public static class ConfigureServices +{ + public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, + IConfiguration configuration) + { + services.Configure(options => { configuration.GetSection("CacheConfiguration"); }); + //For In-Memory Caching + services.AddMemoryCache(); + services.AddTransient(); + services.AddTransient(); + services.AddTransient>(serviceProvider => key => + { + return key switch + { + CacheTypes.Memory => serviceProvider.GetService(), + CacheTypes.Redis => serviceProvider.GetService(), + _ => serviceProvider.GetService() + }; + }); + + return services; + } +} diff --git a/src/Subjects/Common.Persistence/Repository/CachedRepository.cs b/src/Subjects/Common.Persistence/Repository/CachedRepository.cs new file mode 100644 index 00000000..fdeb41be --- /dev/null +++ b/src/Subjects/Common.Persistence/Repository/CachedRepository.cs @@ -0,0 +1,62 @@ +using Goodtocode.Common.Persistence.Cache; +using Microsoft.EntityFrameworkCore; + +namespace Goodtocode.Common.Persistence.Repository; + +public class CachedRepository : ICachedRepository where T : class +{ + private readonly static CacheTypes CacheType = CacheTypes.Memory; + private readonly string cacheKey = $"{typeof(T)}"; + private readonly DbContext _dbContext; + private readonly Func _cacheService; + + public CachedRepository(DbContext dbContext, Func cacheService) + { + _dbContext = dbContext; + _cacheService = cacheService; + } + + public virtual async Task GetByIdAsync(int id) + { + return await _dbContext.Set().FindAsync(id); + } + + public async Task> GetAllAsync() + { + if (!_cacheService(CacheType).TryGet(cacheKey, out IReadOnlyList cachedList)) + { + cachedList = await _dbContext.Set().ToListAsync(); + _cacheService(CacheType).Set(cacheKey, cachedList); + } + return cachedList; + } + + public async Task AddAsync(T entity) + { + await _dbContext.Set().AddAsync(entity); + await _dbContext.SaveChangesAsync(); + await RefreshCache(); + return entity; + } + + public async Task UpdateAsync(T entity) + { + _dbContext.Entry(entity).State = EntityState.Modified; + await _dbContext.SaveChangesAsync(); + await RefreshCache(); + } + + public async Task DeleteAsync(T entity) + { + _dbContext.Set().Remove(entity); + await _dbContext.SaveChangesAsync(); + await RefreshCache(); + } + + public async Task RefreshCache() + { + _cacheService(CacheType).Remove(cacheKey); + var cachedList = await _dbContext.Set().ToListAsync(); + _cacheService(CacheType).Set(cacheKey, cachedList); + } +} diff --git a/src/Subjects/Common.Persistence/Repository/ICachedRepository.cs b/src/Subjects/Common.Persistence/Repository/ICachedRepository.cs new file mode 100644 index 00000000..4711b185 --- /dev/null +++ b/src/Subjects/Common.Persistence/Repository/ICachedRepository.cs @@ -0,0 +1,10 @@ +namespace Goodtocode.Common.Persistence.Repository; + +public interface ICachedRepository where T : class +{ + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task AddAsync(T entity); + Task UpdateAsync(T entity); + Task DeleteAsync(T entity); +} 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 74% rename from src/Subjects/Common/Common.Infrastructure.ApiClient/Common.Infrastructure.ApiClient.csproj rename to src/Subjects/Common/Common.ApiClient/Common.ApiClient.csproj index abe556ec..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 @@ -10,11 +10,10 @@ - - + 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.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.Domain/Common.Domain.csproj b/src/Subjects/Common/Common.Domain/Common.Domain.csproj index dd093679..3c6d4f3e 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/Common/Common.Extensions/Common.Extensions.csproj b/src/Subjects/Common/Common.Extensions/Common.Extensions.csproj new file mode 100644 index 00000000..8919dd76 --- /dev/null +++ b/src/Subjects/Common/Common.Extensions/Common.Extensions.csproj @@ -0,0 +1,11 @@ + + + + Goodtocode.Common.Extensions + Goodtocode.Common.Extensions + net7.0 + enable + enable + + + diff --git a/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultBase.cs b/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultBase.cs new file mode 100644 index 00000000..d74a597f --- /dev/null +++ b/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultBase.cs @@ -0,0 +1,19 @@ +namespace Goodtocode.Common.Extensions; + +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/Common/Common.Extensions/PagedResult/PagedResultT.cs b/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultT.cs new file mode 100644 index 00000000..936e8a74 --- /dev/null +++ b/src/Subjects/Common/Common.Extensions/PagedResult/PagedResultT.cs @@ -0,0 +1,16 @@ +namespace Goodtocode.Common.Extensions; + +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/Common/Common.Extensions/System/ObjectExtensions.cs b/src/Subjects/Common/Common.Extensions/System/ObjectExtensions.cs new file mode 100644 index 00000000..ade6ffa6 --- /dev/null +++ b/src/Subjects/Common/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/Common.Extensions/System/TypeExtensions.cs b/src/Subjects/Common/Common.Extensions/System/TypeExtensions.cs new file mode 100644 index 00000000..6bff6439 --- /dev/null +++ b/src/Subjects/Common/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.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/Core.Application/Business/Queries/GetBusinessQuery.cs b/src/Subjects/Core.Application/Business/Queries/GetBusinessByKeyQuery.cs similarity index 52% rename from src/Subjects/Core.Application/Business/Queries/GetBusinessQuery.cs rename to src/Subjects/Core.Application/Business/Queries/GetBusinessByKeyQuery.cs index 1ff050be..57258670 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessQuery.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessByKeyQuery.cs @@ -6,29 +6,25 @@ 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) { _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..ac20e431 --- /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 PageNumber { get; set; } = 1; + public int PageSize { 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.PageNumber, request.PageSize, + 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..743f70a3 --- /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.PageSize).GreaterThan(0); + RuleFor(x => x.PageNumber).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 f2a943e7..958a627d 100644 --- a/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs +++ b/src/Subjects/Core.Application/Business/Queries/GetBusinessesByNameQuery.cs @@ -1,14 +1,17 @@ -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 PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 20; } -public class GetBusinessesByNameQueryHandler : IRequestHandler> +public class GetBusinessesByNameQueryHandler : IRequestHandler> { private readonly IBusinessRepo _userBusinessesRepo; @@ -17,11 +20,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.PageNumber, request.PageSize, 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..29205dec 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.PageSize).GreaterThan(0); + RuleFor(x => x.PageNumber).GreaterThan(0); } } \ No newline at end of file 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/Core.Application/Interfaces/IBusinessRepo.cs b/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs index b89afc31..f606bc9c 100644 --- a/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs +++ b/src/Subjects/Core.Application/Interfaces/IBusinessRepo.cs @@ -1,14 +1,18 @@ using CSharpFunctionalExtensions; +using Goodtocode.Common.Extensions; using Goodtocode.Subjects.Domain; namespace Goodtocode.Subjects.Application; public interface IBusinessRepo { - Task> GetBusinessAsync(Guid businessKey, + Task> GetBusinessByKeyAsync(Guid businessKey, CancellationToken cancellationToken); - Task>> GetBusinessesByNameAsync(string businessName, + 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/Core.Application/Interfaces/ISubjectsDbContext.cs b/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs index 4cfa325d..8cbf930f 100644 --- a/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs +++ b/src/Subjects/Core.Application/Interfaces/ISubjectsDbContext.cs @@ -5,28 +5,28 @@ namespace Goodtocode.Subjects.Application; public interface ISubjectsDbContext { - DbSet Business { get; set; } - //DbSet Detail { get; set; } - //DbSet DetailType { get; set; } - //DbSet Associate { get; set; } - //DbSet AssociateDetail { get; set; } - //DbSet AssociateOption { get; set; } - //DbSet Gender { get; set; } - //DbSet Government { get; set; } - //DbSet Item { get; set; } - //DbSet ItemGroup { get; set; } - //DbSet ItemType { get; set; } - //DbSet Option { get; set; } - //DbSet OptionGroup { get; set; } - //DbSet Person { get; set; } - //DbSet Resource { get; set; } - //DbSet ResourceItem { get; set; } - //DbSet ResourcePerson { get; set; } - //DbSet ResourceType { get; set; } - //DbSet Venture { get; set; } - //DbSet VentureDetail { get; set; } - //DbSet VentureAssociateOption { get; set; } - //DbSet VentureOption { get; set; } - //DbSet VentureResource { get; set; } + DbSet Business { get; } + //DbSet Detail { get; } + //DbSet DetailType { get; } + //DbSet Associate { get; } + //DbSet AssociateDetail { get; } + //DbSet AssociateOption { get; } + //DbSet Gender { get; } + //DbSet Government { get; } + //DbSet Item { get; } + //DbSet ItemGroup { get; } + //DbSet ItemType { get; } + //DbSet Option { get; } + //DbSet OptionGroup { get; } + //DbSet Person { get; } + //DbSet Resource { get; } + //DbSet ResourceItem { get; } + //DbSet ResourcePerson { get; } + //DbSet ResourceType { get; } + //DbSet Venture { get; } + //DbSet VentureDetail { get; } + //DbSet VentureAssociateOption { get; } + //DbSet VentureOption { get; } + //DbSet VentureResource { get; } Task SaveChangesAsync(CancellationToken cancellationToken); } \ No newline at end of file diff --git a/src/Subjects/Dockerfile b/src/Subjects/Dockerfile new file mode 100644 index 00000000..e42b7fa8 --- /dev/null +++ b/src/Subjects/Dockerfile @@ -0,0 +1,35 @@ +### PREPARE +FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build-env +WORKDIR /src + +### Copy csproj and sln and restore as distinct layers +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 "Goodtocode.Subjects.sln" -c Release -o /app/out + +### RUNTIME IMAGE +FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS runtime-env +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 4142f06b..bce7f166 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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "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 @@ -27,6 +29,10 @@ 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("{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.Persistence", "Common.Persistence\Common.Persistence.csproj", "{718B8C94-7465-432F-AAE2-071088E6E740}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +47,10 @@ Global {DCD762D0-8543-4436-969C-975A94833F20}.Debug|Any CPU.Build.0 = Debug|Any CPU {DCD762D0-8543-4436-969C-975A94833F20}.Release|Any CPU.ActiveCfg = Release|Any CPU {DCD762D0-8543-4436-969C-975A94833F20}.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 {38DDD951-3F89-41FE-A225-326DA46A39CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38DDD951-3F89-41FE-A225-326DA46A39CF}.Debug|Any CPU.Build.0 = Debug|Any CPU {38DDD951-3F89-41FE-A225-326DA46A39CF}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -77,6 +87,14 @@ 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 + {718B8C94-7465-432F-AAE2-071088E6E740}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {718B8C94-7465-432F-AAE2-071088E6E740}.Debug|Any CPU.Build.0 = Debug|Any CPU + {718B8C94-7465-432F-AAE2-071088E6E740}.Release|Any CPU.ActiveCfg = Release|Any CPU + {718B8C94-7465-432F-AAE2-071088E6E740}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -84,7 +102,9 @@ Global GlobalSection(NestedProjects) = preSolution {D5BF79A6-68B3-41B0-AB63-28E30DD87018} = {5D8887CA-2929-4E91-9409-B3366F968527} {DCD762D0-8543-4436-969C-975A94833F20} = {5D8887CA-2929-4E91-9409-B3366F968527} + {1301D19A-3E5A-4EA2-BFC0-625DAA7436AC} = {5D8887CA-2929-4E91-9409-B3366F968527} {38DDD951-3F89-41FE-A225-326DA46A39CF} = {5D8887CA-2929-4E91-9409-B3366F968527} + {718B8C94-7465-432F-AAE2-071088E6E740} = {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..dd7b41a6 --- /dev/null +++ b/src/Subjects/Infrastructure.Persistence/Common/PagedResultExtensions.cs @@ -0,0 +1,24 @@ +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 + { + CurrentPage = page, + PageSize = pageSize, + 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/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 Option { get; set; } - //public virtual DbSet OptionGroup { get; set; } - //public virtual DbSet Person { get; set; } - //public virtual DbSet Resource { get; set; } - //public virtual DbSet ResourceItem { get; set; } - //public virtual DbSet ResourcePerson { get; set; } - //public virtual DbSet ResourceType { get; set; } - //public virtual DbSet Venture { get; set; } - //public virtual DbSet VentureDetail { get; set; } - //public virtual DbSet VentureAssociateOption { get; set; } - //public virtual DbSet VentureOption { get; set; } - //public virtual DbSet VentureResource { get; set; } + public virtual DbSet Business => Set(); + //public virtual DbSet Detail => Set(); + //public virtual DbSet DetailType => Set(); + //public virtual DbSet Associate => Set(); + //public virtual DbSet AssociateDetail => Set(); + //public virtual DbSet AssociateOption => Set(); + //public virtual DbSet Gender => Set(); + //public virtual DbSet Government => Set(); + //public virtual DbSet Item => Set(); + //public virtual DbSet ItemGroup => Set(); + //public virtual DbSet ItemType => Set(); + //public virtual DbSet Option => Set(); + //public virtual DbSet OptionGroup => Set(); + //public virtual DbSet Person => Set(); + //public virtual DbSet Resource => Set(); + //public virtual DbSet ResourceItem => Set(); + //public virtual DbSet ResourcePerson => Set(); + //public virtual DbSet ResourceType => Set(); + //public virtual DbSet Venture => Set(); + //public virtual DbSet VentureDetail => Set(); + //public virtual DbSet VentureAssociateOption => Set(); + //public virtual DbSet VentureOption => Set(); + //public virtual DbSet VentureResource => Set(); public override async Task SaveChangesAsync(CancellationToken cancellationToken = new()) { 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/Infrastructure.Persistence/Repositories/BusinessRepo.cs b/src/Subjects/Infrastructure.Persistence/Repositories/BusinessRepo.cs index 226c8263..a6541467 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; @@ -16,7 +17,7 @@ public BusinessRepo(ISubjectsDbContext context) _context = context; } - 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) @@ -25,9 +26,20 @@ public BusinessRepo(ISubjectsDbContext context) return Result.Failure("Business not found."); } - public async Task>> GetBusinessesByNameAsync(string businessName, CancellationToken cancellationToken) + public async Task>> GetBusinessesAllAsync(string businessName, int page, int results, CancellationToken cancellationToken) { - var businessResult = await _context.Business.Where(x => x.BusinessName == businessName).ToListAsync(cancellationToken); + var businessResult = await _context.Business.AsNoTracking() + .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.AsNoTracking() + .Where(b => b.BusinessName.Contains(businessName)) + .OrderBy(b => b.BusinessKey) + .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 54f88e13..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 }); @@ -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); } /// @@ -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.Api.WebApi/Controllers/BusinessesController.cs b/src/Subjects/Presentation.Api.WebApi/Controllers/BusinessesController.cs index 6c4ef3f1..5cc0d614 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,11 +25,9 @@ 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 = 10) => await Mediator.Send(new GetBusinessesByNameQuery { - return await Mediator.Send(new GetBusinessesByNameQuery - { - BusinessName = name - }); - } + BusinessName = name ?? string.Empty, + PageNumber = pageNumber, + }); } \ 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..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 + + + 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.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.Shared.Rcl/Alerts/Alert.razor b/src/Subjects/Presentation.Shared.Rcl/Alerts/Alert.razor new file mode 100644 index 00000000..ca094db7 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alerts/Alert.razor @@ -0,0 +1,120 @@ +@using Goodtocode.Subjects.Alerts; +@using Microsoft.AspNetCore.Components.Routing; +@implements IDisposable +@inject IAlertService AlertService +@inject NavigationManager NavigationManager + +@namespace Goodtocode.Subjects.Alerts + +@foreach (var alert in alerts) +{ + + RemoveAlert(alert))">× + @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[AlertTypes.Success] = "alert-success"; + alertTypeClass[AlertTypes.Error] = "alert-danger"; + alertTypeClass[AlertTypes.Info] = "alert-info"; + alertTypeClass[AlertTypes.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/Alerts/AlertModel.cs b/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertModel.cs new file mode 100644 index 00000000..8283f1a9 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertModel.cs @@ -0,0 +1,11 @@ +namespace Goodtocode.Subjects.Alerts; + +public class AlertModel +{ + public string Id { 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; } +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertService.cs b/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertService.cs new file mode 100644 index 00000000..d52e91f5 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertService.cs @@ -0,0 +1,73 @@ +namespace Goodtocode.Subjects.Alerts; + +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 = AlertTypes.Success, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Error(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertTypes.Error, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Info(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertTypes.Info, + Message = message, + KeepAfterRouteChange = keepAfterRouteChange, + AutoClose = autoClose + }); + } + + public void Warn(string message, bool keepAfterRouteChange = false, bool autoClose = true) + { + this.Alert(new AlertModel + { + Type = AlertTypes.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/Alerts/AlertTypes.cs b/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertTypes.cs new file mode 100644 index 00000000..daee76fc --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alerts/AlertTypes.cs @@ -0,0 +1,9 @@ +namespace Goodtocode.Subjects.Alerts; + +public enum AlertTypes +{ + Success, + Error, + Info, + Warning +} \ No newline at end of file diff --git a/src/Subjects/Presentation.Shared.Rcl/Alerts/SimpleAlert.razor b/src/Subjects/Presentation.Shared.Rcl/Alerts/SimpleAlert.razor new file mode 100644 index 00000000..f3e22b80 --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Alerts/SimpleAlert.razor @@ -0,0 +1,33 @@ +@using Goodtocode.Common.Extensions; + +@namespace Goodtocode.Subjects.Alerts + +@Message + +@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.Shared.Rcl/Alerts/SimpleAlert.razor.css b/src/Subjects/Presentation.Shared.Rcl/Alerts/SimpleAlert.razor.css new file mode 100644 index 00000000..e69de29b 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..b71a8e8d --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/ExampleJsInterop.cs @@ -0,0 +1,36 @@ +using Microsoft.JSInterop; + +namespace Goodtocode.Subjects; + +// 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/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/Navigation/BusinessRoutes.cs b/src/Subjects/Presentation.Shared.Rcl/Navigation/BusinessRoutes.cs new file mode 100644 index 00000000..ce63e21d --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Navigation/BusinessRoutes.cs @@ -0,0 +1,12 @@ +namespace Goodtocode.Subjects.Navigation; + +public struct BusinessRoutes +{ + public const string BusinessList = "/businesses"; + public const string BusinessSearch = "/businesses/search"; + public const string BusinessDetails = "/businesses/details"; + public const string BusinessCreate = "/businesses/create"; + public const string BusinessEdit = "/businesses/edit"; + public const string BusinessDelete = "/businesses/delete"; +} + 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..eb8b1f7c --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/PageHistoryState.cs @@ -0,0 +1,32 @@ +namespace Goodtocode.Subjects.Paging; + +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/Pager.razor b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor new file mode 100644 index 00000000..3f43ceac --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Paging/Pager.razor @@ -0,0 +1,54 @@ +@using Goodtocode.Common.Extensions; + +@namespace Goodtocode.Subjects.Paging + +@if (Result != null) +{ + + + @if (Result.PageCount > 1) + { + + PagerButtonClicked(1))" class="btn">« + @for (var i = StartIndex; i <= FinishIndex; i++) + { + var currentIndex = i; + @if (i == Result.CurrentPage) + { + @i + } + else + { + PagerButtonClicked(currentIndex))" class="btn">@i + } + } + PagerButtonClicked(Result.PageCount))" class="btn">» + 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..0b7ee88e --- /dev/null +++ b/src/Subjects/Presentation.Shared.Rcl/Presentation.Shared.Rcl.csproj @@ -0,0 +1,24 @@ + + + + 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 00000000..e15a3bde Binary files /dev/null and b/src/Subjects/Presentation.Shared.Rcl/wwwroot/background.png differ 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/App.razor b/src/Subjects/Presentation.Web.BlazorServer/App.razor index 7431d6a1..6a2e6743 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/App.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/App.razor @@ -1,4 +1,5 @@ - +@using Goodtocode.Subjects.BlazorServer.Shared; + diff --git a/src/Subjects/Presentation.Web.BlazorServer/Data/BusinessService.cs b/src/Subjects/Presentation.Web.BlazorServer/Data/BusinessService.cs deleted file mode 100644 index 3d920d04..00000000 --- a/src/Subjects/Presentation.Web.BlazorServer/Data/BusinessService.cs +++ /dev/null @@ -1,90 +0,0 @@ -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 System.Net; -using System.Text.Json; - -namespace Goodtocode.Subjects.BlazorServer.Data; - -public class BusinessService -{ - private readonly IHttpClientFactory _clientFactory; - - public BusinessService(IHttpClientFactory clientFactory) - { - _clientFactory = 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 business = new BusinessEntity(); - if (response.StatusCode != HttpStatusCode.NotFound) - { - response.EnsureSuccessStatusCode(); - business = JsonSerializer.Deserialize(response.Content.ReadAsStream()); - if (business == null) - throw new Exception(); - } - return business; - } - - public async Task> GetBusinessesAsync(string name) - { - var business = new List(); - 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."); - } - - return business; - } - - public async Task CreateBusinessAsync(BusinessObject 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."); - - response.EnsureSuccessStatusCode(); - - return businessCreated; - } - - public async Task UpdateBusinessAsync(BusinessUpdateModel 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) - { - 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/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..a298c84b --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessModel.cs @@ -0,0 +1,15 @@ +using Goodtocode.Subjects.Domain; +using System.ComponentModel.DataAnnotations; + +namespace Goodtocode.Subjects.Models; + +public class BusinessModel : IBusinessEntity +{ + 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; + 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..08830bda --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessTypes.cs @@ -0,0 +1,14 @@ +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..d790f6df --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Models/BusinessValidator.cs @@ -0,0 +1,16 @@ +using FluentValidation; + +namespace Goodtocode.Subjects.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/Models/SearchModel.cs b/src/Subjects/Presentation.Web.BlazorServer/Models/SearchModel.cs new file mode 100644 index 00000000..1c5bcba4 --- /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.Models; + +public class SearchModel +{ + [Required] + public string Name { 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 85d06c63..e77400e6 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessCreate.razor @@ -1,7 +1,8 @@ -@page "/businesscreate" -@using Goodtocode.Subjects.BlazorServer.Data; -@using Goodtocode.Subjects.BlazorServer.Models; +@page "/business/create" +@using Goodtocode.Subjects.Alerts; +@using Goodtocode.Subjects.Data; @using Goodtocode.Subjects.Domain; +@using Goodtocode.Subjects.Models; @using System.ComponentModel.DataAnnotations; @inject BusinessService Service @@ -16,7 +17,7 @@ Name - @@ -24,14 +25,15 @@ Create - - @alertMessage + + @code { - private BusinessCreateModel business = new BusinessCreateModel(); + private BusinessModel business = new BusinessModel(); private string alertMessage = string.Empty; - private CancellationTokenSource cts = new CancellationTokenSource(); + private AlertTypes alertType = AlertTypes.Error; + private CancellationTokenSource? cts; private async Task CreateBusineses() { if (!Validator.TryValidateObject(business, @@ -45,6 +47,7 @@ alertMessage = string.Empty; await Service.CreateBusinessAsync(business); alertMessage = $"{business.BusinessName} created"; + alertType = AlertTypes.Success; } catch (TaskCanceledException) { 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..377ad3d8 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessForm.razor @@ -0,0 +1,79 @@ +@using Goodtocode.Subjects.Models; +@using Goodtocode.Subjects.Paging; +@inject Goodtocode.Subjects.Paging.PageHistoryState PageHistoryState + + + + + Business Name: + + + + + + + Tax Number: + + + + + + + + Business Type: + + + --Select-- + Sole proprietorship + Partnership + Corporation + Multi-national corporation + Not for profit organizations + Franchise + Limited Liability Company + Trust + Other + + + + + + + + Phone Number : + + + + + + + + + + + @if (loading) + { + + } + @ButtonText + + @if (PageHistoryState.CanGoBack()){ + Cancel + } + else{ + Back + } + + + + +@code { + [Parameter] + public BusinessModel business { get; set; } = new BusinessModel(); + [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..4603c119 100644 --- a/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessList.razor @@ -1,62 +1,100 @@ -@page "/businesssearch" -@using Goodtocode.Subjects.BlazorServer.Data; -@using Goodtocode.Subjects.BlazorServer.Models; +@page "/businesses/{page:int}" +@using Goodtocode.Common.Extensions; +@using Goodtocode.Subjects.Data; +@using Goodtocode.Subjects.Models; @using Goodtocode.Subjects.Domain; @using System.ComponentModel.DataAnnotations; +@using Goodtocode.Subjects.Paging +@using Microsoft.AspNetCore.Http.Extensions; @inject BusinessService Service +@inject PageHistoryState PageHistory +@inject NavigationManager UriHelper -Business Search +Business List - - - - { if (e.Key == "Enter") { await GetBusineses(); } })"> - - - - @alertMessage - - Loading... - - +@alertMessage + + Loading... + -@if (businesses.Count() > 0) +@if (businesses.Results.Count() > 0) { - + Key Name Tax Number + - @foreach (var business in businesses) + @foreach (var business in businesses.Results) { @business.BusinessKey @business.BusinessName @business.TaxNumber + + + + DeleteBusinessAsync(business))" disabled="@business.IsDeleting" class="btn btn-link oi oi-trash p-0 nounderline"> + @if (business.IsDeleting) { } + else { } + + } + } @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 SearchModel businessSearch = new SearchModel(); + 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() + { + await GetBusinessesAsync(); + 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") + { + 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; @@ -65,9 +103,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, page); + if (businesses.Results.Count() == 0) alertMessage = "No businesses found"; } catch (TaskCanceledException) @@ -77,6 +115,32 @@ 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/Pages/Business/BusinessSearch.razor b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessSearch.razor new file mode 100644 index 00000000..825cf557 --- /dev/null +++ b/src/Subjects/Presentation.Web.BlazorServer/Pages/Business/BusinessSearch.razor @@ -0,0 +1,148 @@ +@page "/businesses/search" +@using Goodtocode.Common.Extensions; +@using Goodtocode.Subjects.Data; +@using Goodtocode.Subjects.Domain; +@using Goodtocode.Subjects.Models; +@using Goodtocode.Subjects.Paging; +@using Microsoft.AspNetCore.Http.Extensions; +@using System.ComponentModel.DataAnnotations; + +@inject BusinessService Service +@inject PageHistoryState PageHistory +@inject NavigationManager UriHelper + +Business Search + + + + +