diff --git a/zero.Raven.IdentityDemo/ScaffoldingReadMe.txt b/zero.Raven.IdentityDemo/ScaffoldingReadMe.txt deleted file mode 100644 index 6e6208dc..00000000 --- a/zero.Raven.IdentityDemo/ScaffoldingReadMe.txt +++ /dev/null @@ -1,3 +0,0 @@ -Support for ASP.NET Core Identity was added to your project. - -For setup and configuration information, see https://go.microsoft.com/fwlink/?linkid=2116645. diff --git a/zero.Raven/Extensions/ValidatorExtensions.cs b/zero.Raven/Extensions/ValidatorExtensions.cs index e1f75019..2b90f19f 100644 --- a/zero.Raven/Extensions/ValidatorExtensions.cs +++ b/zero.Raven/Extensions/ValidatorExtensions.cs @@ -69,7 +69,6 @@ public static class ValidatorExtensions /// Check if this reference exists and is an entity which can be referenced /// public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IRavenOperations ops) - where T : ZeroIdEntity where TCollection : ZeroIdEntity { return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => diff --git a/zero.Raven/Operations/RavenOperations.cs b/zero.Raven/Operations/RavenOperations.cs index 35766dab..1311a7e1 100644 --- a/zero.Raven/Operations/RavenOperations.cs +++ b/zero.Raven/Operations/RavenOperations.cs @@ -111,7 +111,8 @@ public partial class RavenOperations : IRavenOperations /// public virtual T WhenActive(T model) where T : ZeroIdEntity, new() { - return model != null && (model is not ZeroEntity || (model as ZeroEntity).IsActive) && (model is not ISupportsSoftDelete || !(model as ISupportsSoftDelete).IsDeleted) ? model : default; + return model; // TODO should we really use this? I tried to get data in a custom backend but couldn't because of this + //return model != null && (model is not ZeroEntity || (model as ZeroEntity).IsActive) && (model is not ISupportsSoftDelete || !(model as ISupportsSoftDelete).IsDeleted) ? model : default; } } diff --git a/zero.Raven/ZeroRavenModule.cs b/zero.Raven/ZeroRavenModule.cs index 40b7b361..39cb1326 100644 --- a/zero.Raven/ZeroRavenModule.cs +++ b/zero.Raven/ZeroRavenModule.cs @@ -31,9 +31,9 @@ internal class ZeroRavenModule : ZeroModule services.AddTransient(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.Replace(ServiceLifetime.Scoped); + services.Replace(ServiceLifetime.Scoped); + services.Replace(ServiceLifetime.Scoped); services.AddOptions(); services.AddOptions().Bind(configuration.GetSection("Zero:Raven")); diff --git a/zero.sln b/zero.sln index 0d456af1..c010669a 100644 --- a/zero.sln +++ b/zero.sln @@ -3,23 +3,15 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31912.275 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Commerce", "plugins\zero.Commerce\zero.Commerce.csproj", "{63A8AD15-15F0-4555-BD62-C38B4465FFC1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Stories", "plugins\zero.Stories\zero.Stories.csproj", "{C23CF90A-DB90-427F-816C-0E2FE20E29D0}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Forms", "plugins\zero.Forms\zero.Forms.csproj", "{A8E5F34F-5400-4F92-96C6-7F91645BC220}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "plugins", "plugins", "{6FAA92A8-CA1A-48EA-9857-C9092971B4D1}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Core", "zero.Core\zero.Core.csproj", "{CED2A7FA-8D35-4A2A-AF57-8B95307547CB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero", "zero\zero.csproj", "{33CD6E46-CD81-42C3-9019-C0EAD557EE99}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero", "zero\zero.csproj", "{33CD6E46-CD81-42C3-9019-C0EAD557EE99}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "legacy", "legacy", "{93A1CCEB-A323-445E-8116-4575C5939B46}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Raven", "zero.Raven\zero.Raven.csproj", "{DC79D7EA-FAA1-4701-9C36-8295CD1C45D0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Raven", "zero.Raven\zero.Raven.csproj", "{DC79D7EA-FAA1-4701-9C36-8295CD1C45D0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "zero.Raven.IdentityDemo", "zero.Raven.IdentityDemo\zero.Raven.IdentityDemo.csproj", "{05C829D6-3052-40DB-9282-3BDFCDAE0F05}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "zero.Raven.IdentityDemo", "zero.Raven.IdentityDemo\zero.Raven.IdentityDemo.csproj", "{05C829D6-3052-40DB-9282-3BDFCDAE0F05}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,18 +19,6 @@ Global Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {63A8AD15-15F0-4555-BD62-C38B4465FFC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {63A8AD15-15F0-4555-BD62-C38B4465FFC1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {63A8AD15-15F0-4555-BD62-C38B4465FFC1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {63A8AD15-15F0-4555-BD62-C38B4465FFC1}.Release|Any CPU.Build.0 = Release|Any CPU - {C23CF90A-DB90-427F-816C-0E2FE20E29D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C23CF90A-DB90-427F-816C-0E2FE20E29D0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C23CF90A-DB90-427F-816C-0E2FE20E29D0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C23CF90A-DB90-427F-816C-0E2FE20E29D0}.Release|Any CPU.Build.0 = Release|Any CPU - {A8E5F34F-5400-4F92-96C6-7F91645BC220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A8E5F34F-5400-4F92-96C6-7F91645BC220}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A8E5F34F-5400-4F92-96C6-7F91645BC220}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A8E5F34F-5400-4F92-96C6-7F91645BC220}.Release|Any CPU.Build.0 = Release|Any CPU {CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Debug|Any CPU.Build.0 = Debug|Any CPU {CED2A7FA-8D35-4A2A-AF57-8B95307547CB}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -60,9 +40,6 @@ Global HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {63A8AD15-15F0-4555-BD62-C38B4465FFC1} = {6FAA92A8-CA1A-48EA-9857-C9092971B4D1} - {C23CF90A-DB90-427F-816C-0E2FE20E29D0} = {6FAA92A8-CA1A-48EA-9857-C9092971B4D1} - {A8E5F34F-5400-4F92-96C6-7F91645BC220} = {6FAA92A8-CA1A-48EA-9857-C9092971B4D1} {CED2A7FA-8D35-4A2A-AF57-8B95307547CB} = {93A1CCEB-A323-445E-8116-4575C5939B46} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/zero/Communication/Messages/MessageAggregator.cs b/zero/Communication/Messages/MessageAggregator.cs index cd6a3f37..2ffb3c2a 100644 --- a/zero/Communication/Messages/MessageAggregator.cs +++ b/zero/Communication/Messages/MessageAggregator.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Linq.Expressions; namespace zero.Communication; @@ -26,6 +27,13 @@ public class MessageAggregator : IMessageAggregator } + /// + public void Subscribe(Expression> handle) where TMessage : class, IMessage + { + Subscription.Add(new InlineMessageSubscription(handle)); + } + + /// public void Activate() where TMessage : class, IMessage @@ -43,6 +51,11 @@ public interface IMessageAggregator /// Task Publish(TMessage message) where TMessage : class, IMessage; + /// + /// Subscribe to a message + /// + void Subscribe(Expression> handle) where TMessage : class, IMessage; + /// /// Subscribes the specified handler to the spified message type /// diff --git a/zero/Communication/Messages/MessageSubscription.cs b/zero/Communication/Messages/MessageSubscription.cs index 3698f175..203825fb 100644 --- a/zero/Communication/Messages/MessageSubscription.cs +++ b/zero/Communication/Messages/MessageSubscription.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using System.Linq.Expressions; using System.Reflection; namespace zero.Communication; @@ -33,6 +34,38 @@ internal class MessageSubscription : IMessageSubscrip } +internal class InlineMessageSubscription : IMessageSubscription + where TMessage : class, IMessage +{ + private Expression> _handler; + + public InlineMessageSubscription(Expression> handler) + { + _handler = handler; + } + + + public bool CanDeliver() + { + return typeof(TMessage).GetTypeInfo().IsAssignableFrom(typeof(T)); + } + + public async Task Deliver(IServiceProvider serviceProvider, object message) + { + if (message == null) + { + throw new ArgumentNullException(nameof(message)); + } + if (!(message is TMessage)) + { + throw new ArgumentException($"{nameof(message)} must be of type '{typeof(TMessage).FullName}'"); + } + + await _handler.Compile()((TMessage)message); + } +} + + public interface IMessageSubscription { bool CanDeliver(); diff --git a/zero/Configuration/ZeroOptions.cs b/zero/Configuration/ZeroOptions.cs index 483bf31a..d469e274 100644 --- a/zero/Configuration/ZeroOptions.cs +++ b/zero/Configuration/ZeroOptions.cs @@ -9,6 +9,9 @@ public class ZeroOptions : IZeroOptions /// public string Version { get; set; } + /// + public string Language { get; set; } = "en_US"; + /// public TimeSpan TokenExpiration { get; set; } @@ -43,6 +46,11 @@ public interface IZeroOptions /// string Version { get; set; } + /// + /// ISO Code for language + /// + public string Language { get; set; } + /// /// Expiration of a generated change token for an entity /// diff --git a/zero/Localization/ConfigurationLocalizer.cs b/zero/Localization/ConfigurationLocalizer.cs index c9379fe0..4fa20668 100644 --- a/zero/Localization/ConfigurationLocalizer.cs +++ b/zero/Localization/ConfigurationLocalizer.cs @@ -1,7 +1,4 @@ -using System.IO; -using Microsoft.AspNetCore.Hosting; -using System.Text; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; namespace zero.Localization; @@ -11,7 +8,7 @@ public class ConfigurationLocalizer : Localizer private IConfiguration _configuration; - public ConfigurationLocalizer(IConfiguration configuration, IOptionsMonitor options) : base() + public ConfigurationLocalizer(IConfiguration configuration, ICultureResolver cultureResolver, IOptionsMonitor options) : base(cultureResolver) { _configuration = configuration; } @@ -19,7 +16,7 @@ public class ConfigurationLocalizer : Localizer protected override Translation LoadTranslation(string key) { - IConfigurationSection section = _configuration.GetSection($"Zero:Localization:{key}"); + IConfigurationSection section = _configuration.GetSection($"Zero:Localization:{LanguageCode}:{key}"); if (section == null) { diff --git a/zero/Localization/CultureChangeMessage.cs b/zero/Localization/CultureChangeMessage.cs new file mode 100644 index 00000000..5f73d7f6 --- /dev/null +++ b/zero/Localization/CultureChangeMessage.cs @@ -0,0 +1,8 @@ +using System.Globalization; + +namespace zero.Localization; + +public class CultureChangeMessage : IMessage +{ + public CultureInfo Culture { get; set; } +} \ No newline at end of file diff --git a/zero/Localization/CultureResolver.cs b/zero/Localization/CultureResolver.cs index c612145a..bbf98a8a 100644 --- a/zero/Localization/CultureResolver.cs +++ b/zero/Localization/CultureResolver.cs @@ -1,57 +1,117 @@ using FluentValidation; +using Microsoft.AspNetCore.Connections.Features; using Microsoft.Extensions.Logging; using System.Globalization; +using System.Linq.Expressions; using System.Security.Claims; namespace zero.Localization; public class CultureResolver : ICultureResolver { + /// + public CultureInfo Current { get; protected set; } + protected ILogger Logger { get; private set; } + protected IMessageAggregator MessageAggregator { get; private set; } - public CultureResolver(ILogger logger) + + public CultureResolver(ILogger logger, IMessageAggregator messageAggregator) { Logger = logger; + MessageAggregator = messageAggregator; } /// public Task Resolve(IZeroContext context) { - //var session = context.Store.Session(); - //Language language = await session.Query().FirstOrDefaultAsync(); - string isoCode = "de-AT"; + if (!TryConvert(context.Options.Language, out CultureInfo culture)) + { + culture = CultureInfo.CurrentCulture; + } + Set(culture); + + return Task.FromResult(culture); + } + + + /// + public bool TryConvert(string isoCode, out CultureInfo culture) + { try { - CultureInfo culture = CultureInfo.CreateSpecificCulture(isoCode); + culture = CultureInfo.CreateSpecificCulture(isoCode.Replace('_', '-')); if (culture.ThreeLetterISOLanguageName.IsNullOrEmpty()) { throw new Exception("ThreeLetterISOLanguageName is empty"); } + if (culture.ThreeLetterISOLanguageName == "ivl") + { + throw new Exception("Invariant language is not allowed"); + } - CultureInfo.CurrentCulture = culture; - CultureInfo.CurrentUICulture = culture; - ValidatorOptions.Global.LanguageManager.Culture = culture; + return true; } catch (Exception ex) { - Logger.LogError(ex, "Could not create culture from Language code {code}", isoCode); - return Task.FromResult(CultureInfo.CurrentCulture); + Logger.LogWarning(ex, "Could not create culture from Language code {code}", isoCode); + culture = null; + return false; } - - return Task.FromResult(CultureInfo.CurrentCulture); + } + + + /// + public void Set(CultureInfo culture) + { + CultureInfo.CurrentCulture = culture; + CultureInfo.CurrentUICulture = culture; + ValidatorOptions.Global.LanguageManager.Culture = culture; + Current = culture; + MessageAggregator.Publish(new CultureChangeMessage() + { + Culture = culture + }); + } + + + /// + public void Subscribe(Expression> handle) + { + MessageAggregator.Subscribe(handle); } } public interface ICultureResolver { + /// + /// Current culture + /// + CultureInfo Current { get; } + /// /// Resolves the current application from either the backoffice user (in case it is backoffice request) /// or the domain (in case it is frontend request). /// Task Resolve(IZeroContext context); + + /// + /// Tries to convert an ISO code to a culture + /// + bool TryConvert(string isoCode, out CultureInfo culture); + + /// + /// Set a new culture for this request + /// + void Set(CultureInfo culture); + + /// + /// Subscribe to culture change + /// + void Subscribe(Expression> handle); } diff --git a/zero/Localization/Localizer.cs b/zero/Localization/Localizer.cs index 5395cee5..d6f51af2 100644 --- a/zero/Localization/Localizer.cs +++ b/zero/Localization/Localizer.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Globalization; using System.Reflection; namespace zero.Localization; @@ -7,6 +8,25 @@ public abstract class Localizer : ILocalizer { protected ConcurrentDictionary Cache { get; private set; } = new(); + protected ICultureResolver CultureResolver { get; private set; } + + protected string LanguageCode { get; set; } + + + public Localizer(ICultureResolver cultureResolver) + { + CultureResolver = cultureResolver; + CultureResolver.Subscribe(msg => OnCultureChange(msg.Culture)); + OnCultureChange(CultureResolver.Current); + } + + + Task OnCultureChange(CultureInfo culture) + { + LanguageCode = culture.Name.Split(new char[] { '_', '-' })[0]; + return Task.CompletedTask; + } + /// public string Text(string key) => Text(key, null); @@ -83,7 +103,7 @@ public abstract class Localizer : ILocalizer } public interface ILocalizer -{ +{ /// /// /// diff --git a/zero/Media/IZeroMediaStoreDbProvider.cs b/zero/Media/IZeroMediaStoreDbProvider.cs index db15fe01..69fe93b2 100644 --- a/zero/Media/IZeroMediaStoreDbProvider.cs +++ b/zero/Media/IZeroMediaStoreDbProvider.cs @@ -13,4 +13,33 @@ public interface IZeroMediaStoreDbProvider Task> Update(T model, CancellationToken ct = default) where T : ZeroEntity, new(); Task> Delete(T model, CancellationToken ct = default) where T : ZeroEntity, new(); +} + + +public class EmptyZeroMediaStoreDbProvider : IZeroMediaStoreDbProvider +{ + public Task> Create(T model, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } + + public Task> Delete(T model, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } + + public Task Find(Expression> expression, CancellationToken ct = default) where T : ZeroEntity + { + throw new NotImplementedException(); + } + + public Task> FindAll(Expression> expression, CancellationToken ct = default) where T : ZeroEntity + { + throw new NotImplementedException(); + } + + public Task> Update(T model, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/zero/Media/ImageSharp/Processors/RotateWebProcessor.cs b/zero/Media/ImageSharp/Processors/RotateWebProcessor.cs new file mode 100644 index 00000000..64eb47d4 --- /dev/null +++ b/zero/Media/ImageSharp/Processors/RotateWebProcessor.cs @@ -0,0 +1,49 @@ +using Microsoft.Extensions.Logging; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Web; +using SixLabors.ImageSharp.Web.Commands; +using SixLabors.ImageSharp.Web.Processors; +using System.Globalization; + +namespace zero.Media.ImageSharp.Processors; + +public class RotateWebProcessor : IImageWebProcessor +{ + // + /// The command constant for quality. + /// + public const string Rotate = "rotate"; + + /// + /// The reusable collection of commands. + /// + private static readonly IEnumerable RotateCommands + = new[] { Rotate }; + + /// + public IEnumerable Commands { get; } = RotateCommands; + + /// + public FormattedImage Process( + FormattedImage image, + ILogger logger, + CommandCollection commands, + CommandParser parser, + CultureInfo culture) + { + if (commands.Contains(Rotate)) + { + float degrees = parser.ParseValue(commands.GetValueOrDefault(Rotate), culture); + + if (degrees != 0) + { + image.Image.Mutate(x => x.Rotate(degrees)); + } + } + + return image; + } + + /// + public bool RequiresTrueColorPixelFormat(CommandCollection commands, CommandParser parser, CultureInfo culture) => true; +} diff --git a/zero/Media/MediaManagement.cs b/zero/Media/MediaManagement.cs index c01894c8..542ed944 100644 --- a/zero/Media/MediaManagement.cs +++ b/zero/Media/MediaManagement.cs @@ -83,6 +83,10 @@ public class MediaManagement : IMediaManagement /// public virtual async Task> DeleteFile(Media file) { + if (file == null) + { + return Result.Success(); + } await FileSystem.DeleteDirectory(file.FileId, true); return await Db.Delete(file); } @@ -90,11 +94,11 @@ public class MediaManagement : IMediaManagement /// - public virtual async Task> UploadFile(Stream fileStream, string filename, string folderId = null, Action onBeforeSave = null, CancellationToken cancellationToken = default) + public virtual async Task> UploadFile(Stream fileStream, string filename, string folderId = null, Action onBeforeSave = null, bool persist = true, CancellationToken cancellationToken = default) { Result result = await Creator.UploadFile(fileStream, filename, folderId, cancellationToken); - if (!result.IsSuccess) + if (!result.IsSuccess || !persist) { return result; } @@ -180,7 +184,7 @@ public interface IMediaManagement /// /// Uploads a file and persists it /// - Task> UploadFile(Stream fileStream, string filename, string folderId = null, Action onBeforeSave = null, CancellationToken cancellationToken = default); + Task> UploadFile(Stream fileStream, string filename, string folderId = null, Action onBeforeSave = null, bool persist = true, CancellationToken cancellationToken = default); /// /// Get a media folder by id diff --git a/zero/Media/MediaManagementExtensions.cs b/zero/Media/MediaManagementExtensions.cs index 3fc4f3ef..aca499ec 100644 --- a/zero/Media/MediaManagementExtensions.cs +++ b/zero/Media/MediaManagementExtensions.cs @@ -36,20 +36,20 @@ namespace zero.Media /// /// Uploads a file and persists it /// - public static async Task> UploadFile(this IMediaManagement media, IFormFile formFile, string folderId = null, Action onBeforeSave = null, CancellationToken cancellationToken = default) + public static async Task> UploadFile(this IMediaManagement media, IFormFile formFile, string folderId = null, Action onBeforeSave = null, bool persist = true, CancellationToken cancellationToken = default) { using Stream stream = formFile.OpenReadStream(); - return await media.UploadFile(stream, formFile.FileName, folderId, onBeforeSave, cancellationToken); + return await media.UploadFile(stream, formFile.FileName, folderId, onBeforeSave, persist, cancellationToken); } /// /// Uploads a file and persists it /// - public static async Task> UploadFile(this IMediaManagement media, byte[] fileBytes, string filename, string folderId = null, Action onBeforeSave = null, CancellationToken cancellationToken = default) + public static async Task> UploadFile(this IMediaManagement media, byte[] fileBytes, string filename, string folderId = null, Action onBeforeSave = null, bool persist = true, CancellationToken cancellationToken = default) { using Stream stream = new MemoryStream(fileBytes); - return await media.UploadFile(stream, filename, folderId, onBeforeSave, cancellationToken); + return await media.UploadFile(stream, filename, folderId, onBeforeSave, persist, cancellationToken); } diff --git a/zero/Media/ZeroMediaModule.cs b/zero/Media/ZeroMediaModule.cs index 7dabfc9b..5aabadf2 100644 --- a/zero/Media/ZeroMediaModule.cs +++ b/zero/Media/ZeroMediaModule.cs @@ -9,6 +9,8 @@ using Microsoft.AspNetCore.Routing; using SixLabors.ImageSharp.Web.Caching; using SixLabors.ImageSharp.Web.DependencyInjection; using zero.Media.ImageSharp; +using zero.Numbers; +using zero.Media.ImageSharp.Processors; namespace zero.Media; @@ -20,6 +22,7 @@ internal class ZeroMediaModule : ZeroModule .SetRequestParser() .ClearProviders() .AddProvider() + .AddProcessor() .Configure(configuration.GetSection("Zero:Media:ImageSharp:Cache")); //configuration.AddJsonFile(Path.Combine(Directory.GetCurrentDirectory(), "Config/imaging.json"), true, true); @@ -35,6 +38,7 @@ internal class ZeroMediaModule : ZeroModule services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddOptions().Bind(configuration.GetSection("Zero:Media")).Configure(opts => { diff --git a/zero/Numbers/IZeroNumberStoreDbProvider.cs b/zero/Numbers/IZeroNumberStoreDbProvider.cs index b717e47f..51f4ad35 100644 --- a/zero/Numbers/IZeroNumberStoreDbProvider.cs +++ b/zero/Numbers/IZeroNumberStoreDbProvider.cs @@ -1,4 +1,5 @@ using System.Linq.Expressions; +using zero.Numbers; namespace zero.Numbers; @@ -15,4 +16,38 @@ public interface IZeroNumberStoreDbProvider Task> Update(T model, CancellationToken ct = default) where T : ZeroEntity, new(); Task> Delete(T model, CancellationToken ct = default) where T : ZeroEntity, new(); +} + + +public class EmptyZeroNumberStoreDbProvider : IZeroNumberStoreDbProvider +{ + public Task> Create(T model, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } + + public Task> Delete(T model, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } + + public Task Find(Expression> expression, CancellationToken ct = default) where T : ZeroEntity + { + throw new NotImplementedException(); + } + + public Task> FindAll(Expression> expression, CancellationToken ct = default) where T : ZeroEntity + { + throw new NotImplementedException(); + } + + public Task Load(string id, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } + + public Task> Update(T model, CancellationToken ct = default) where T : ZeroEntity, new() + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/zero/Numbers/Numbers.cs b/zero/Numbers/Numbers.cs index 17901643..06c450d5 100644 --- a/zero/Numbers/Numbers.cs +++ b/zero/Numbers/Numbers.cs @@ -22,7 +22,14 @@ public class Numbers : INumbers /// - public async Task Next(string alias, DateTimeOffset? date = null, bool store = true) + public Task Next(string alias, DateTimeOffset? date = null, bool store = true) + { + return Next(alias, date.HasValue ? DateOnly.FromDateTime(date.Value.Date) : null, store); + } + + + /// + public async Task Next(string alias, DateOnly? date = null, bool store = true) { Number number = await Get(alias); string key = GetCounterKey(number); @@ -92,10 +99,17 @@ public class Numbers : INumbers /// public string Compile(string template, long value, int minLength, DateTimeOffset? date = null) + { + return Compile(template, value, minLength, date.HasValue ? DateOnly.FromDateTime(date.Value.Date) : null); + } + + + /// + public string Compile(string template, long value, int minLength, DateOnly? date = null) { string output = template; MatchCollection matches = templateRegex.Matches(output); - DateTimeOffset usedDate = date ?? DateTimeOffset.Now; + DateOnly usedDate = date ?? DateOnly.FromDateTime(DateTime.Now); foreach (Match match in matches) { @@ -228,6 +242,11 @@ public interface INumbers /// Task Next(string alias, DateTimeOffset? date = null, bool store = true); + /// + /// Get the next available number for the specified template + /// + Task Next(string alias, DateOnly? date = null, bool store = true); + /// /// Resets the current counter for the specified template /// @@ -242,4 +261,9 @@ public interface INumbers /// Renders the final output from the template and the value /// string Compile(string template, long value, int minLength, DateTimeOffset? date = null); + + /// + /// Renders the final output from the template and the value + /// + string Compile(string template, long value, int minLength, DateOnly? date = null); } \ No newline at end of file diff --git a/zero/Numbers/ZeroNumberModule.cs b/zero/Numbers/ZeroNumberModule.cs index cce42dd8..c79787ad 100644 --- a/zero/Numbers/ZeroNumberModule.cs +++ b/zero/Numbers/ZeroNumberModule.cs @@ -12,5 +12,6 @@ internal class ZeroNumberModule : ZeroModule services.AddScoped(); services.AddScoped, NumberValidator>(); + services.AddScoped(); } } \ No newline at end of file diff --git a/zero/Routing/RequestUrlResolver.cs b/zero/Routing/RequestUrlResolver.cs new file mode 100644 index 00000000..0e605b5b --- /dev/null +++ b/zero/Routing/RequestUrlResolver.cs @@ -0,0 +1,128 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace zero.Routing; + +public class RequestUrlResolver : IRequestUrlResolver +{ + protected IHttpContextAccessor HttpContextAccessor { get; private set; } + + protected ILogger Logger { get; private set; } + + static string[] Protocols = new[] { "http://", "https://", "ftp://", "ftps://", "sftp://", "udp://" }; + + + public RequestUrlResolver(IHttpContextAccessor httpContextAccessor, ILogger logger) + { + HttpContextAccessor = httpContextAccessor; + Logger = logger; + } + + + /// + public bool IsAbsolute(string path) + { + if (!CanResolve()) + { + return false; + } + + if (String.IsNullOrWhiteSpace(path)) + { + return false; + } + + if (Protocols.Any(p => path.StartsWith(p, StringComparison.InvariantCultureIgnoreCase))) + { + return true; + } + + return false; + } + + + /// + public string ToAbsolute(string path) + { + if (!CanResolve()) + { + return null; + } + + if (String.IsNullOrWhiteSpace(path)) + { + return GetRoot(); + } + + if (Protocols.Any(p => path.StartsWith(p, StringComparison.InvariantCultureIgnoreCase))) + { + return path; + } + + path = path.Trim().TrimEnd('/'); + + if (path.StartsWith("~/")) + { + path = path.Substring(1); + } + if (!path.StartsWith("/")) + { + return GetRoot(includePath: true) + '/' + path; + } + + return GetRoot() + path; + } + + + /// + public string GetRoot(bool includePath = false) + { + if (!CanResolve()) + { + Logger.LogWarning("Tried to resolve an URL but no HttpRequest is available"); + return null; + } + + HttpRequest request = HttpContextAccessor.HttpContext.Request; + return $"{request.Scheme}://{request.Host.Host}{GetPortUrlPart(request)}{(includePath ? request.PathBase.ToUriComponent() : String.Empty)}"; + } + + + /// + /// Require a request in order to resolve the URL + /// + protected bool CanResolve() => HttpContextAccessor?.HttpContext?.Request != null; + + + /// + /// Resolves the port as part of the URL + /// + protected string GetPortUrlPart(HttpRequest request) + { + if (!request.Host.Port.HasValue || request.Host.Port == 80 || (request.Host.Port == 443 && request.IsHttps)) + { + return String.Empty; + } + + return ":" + request.Host.Port; + } +} + + +public interface IRequestUrlResolver +{ + /// + /// Checks whether a path is absolute or relative + /// + bool IsAbsolute(string path); + + /// + /// Converts a relative to an absolute path for the currently used domain + /// + string ToAbsolute(string path); + + /// + /// Get protocol + domain and optional port + /// + string GetRoot(bool includePath = false); +} diff --git a/zero/Routing/ZeroRoutingModule.cs b/zero/Routing/ZeroRoutingModule.cs new file mode 100644 index 00000000..8ff86cbb --- /dev/null +++ b/zero/Routing/ZeroRoutingModule.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Routing; + +internal class ZeroRoutingModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(); + } +} \ No newline at end of file diff --git a/zero/Utils/IdGenerator.cs b/zero/Utils/IdGenerator.cs index 49367473..86155bd2 100644 --- a/zero/Utils/IdGenerator.cs +++ b/zero/Utils/IdGenerator.cs @@ -4,8 +4,9 @@ namespace zero.Utils; public class IdGenerator { - const string CHARS = "abcdefghijklmnopqrstuvwxyz0123456789"; - const string CHARS_COMPLEX = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@#.:!?*"; + const string CHARS_az09 = "abcdefghijklmnopqrstuvwxyz0123456789"; + const string CHARS_azAZ09x = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@#.:!?*"; + const string CHARS_x09 = "0123456789"; private static Random random = new(); @@ -19,7 +20,7 @@ public class IdGenerator length = 12; } - string chars = charset == Charset.az09 ? CHARS : CHARS_COMPLEX; + string chars = charset == Charset.az09 ? CHARS_az09 : charset == Charset.x09 ? CHARS_x09 : CHARS_azAZ09x; return new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); @@ -107,6 +108,10 @@ public class IdGenerator /// /// a-z, A-Z, 0-9, _-@#.:!?* /// - azAZ09x = 1 + azAZ09x = 1, + /// + /// 0-9 + /// + x09 = 2 } } \ No newline at end of file diff --git a/zero/ZeroBuilder.cs b/zero/ZeroBuilder.cs index 987213e3..4569b257 100644 --- a/zero/ZeroBuilder.cs +++ b/zero/ZeroBuilder.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using zero.Mvc; using zero.Numbers; +using zero.Routing; namespace zero; @@ -57,7 +58,7 @@ public class ZeroBuilder //Modules.Add(); Modules.Add(); Modules.Add(); - //Modules.Add(); + Modules.Add(); Modules.ConfigureServices(services, configuration);