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