Skip to content

Commit

Permalink
Merge branch 'release/v7.28.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
cleftheris committed Jul 26, 2024
2 parents eb64368 + 20d75d9 commit 8ab35e0
Show file tree
Hide file tree
Showing 45 changed files with 4,436 additions and 3,847 deletions.
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<PropertyGroup Label="Assembly Common">
<Copyright>Copyright (c) 2018 Indice</Copyright>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks>
<VersionPrefixBase>7.26</VersionPrefixBase>
<VersionPrefixBase>7.28</VersionPrefixBase>
<VersionPrefixCore>$(VersionPrefixBase).0</VersionPrefixCore>
<VersionPrefixIdentity>$(VersionPrefixBase).0</VersionPrefixIdentity>
<VersionPrefixIdentityUI>$(VersionPrefixBase).0</VersionPrefixIdentityUI>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Options;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Text.RegularExpressions;

namespace Microsoft.AspNetCore.Routing;

Expand All @@ -29,9 +30,10 @@ public static IServiceCollection AddTranslationGraph(this IServiceCollection ser
var options = new TranslationsGraphOptions();
configureAction?.Invoke(options);
services.Configure<TranslationsGraphOptions>((o) => {
o.TranslationsBaseName = options.TranslationsBaseName;
o.TranslationsLocation = options.TranslationsLocation;
o.EndpointRoutePattern = options.EndpointRoutePattern;
o.DefaultTranslationsBaseName = options.DefaultTranslationsBaseName;
o.DefaultTranslationsLocation = options.DefaultTranslationsLocation;
o.DefaultEndpointRoutePattern = options.DefaultEndpointRoutePattern;
o.Resources.AddRange(options.Resources);
});
return services;
}
Expand All @@ -44,10 +46,14 @@ public static IServiceCollection AddTranslationGraph(this IServiceCollection ser

public static IEndpointRouteBuilder MapTranslationGraph(this IEndpointRouteBuilder routes) {
var options = routes.ServiceProvider.GetRequiredService<IOptions<TranslationsGraphOptions>>().Value;
routes.MapGet(options.EndpointRoutePattern, (string lang, IStringLocalizerFactory factory) => {
var strings = factory.Create(options.TranslationsBaseName, options.TranslationsLocation);
return TypedResults.Ok(strings.ToObjectGraph(new System.Globalization.CultureInfo(lang)));
});
var endpoints = options.GetEndpoints();
foreach (var endpoint in endpoints) {
routes.MapGet(endpoint.Key, (string lang, IStringLocalizerFactory factory) => {
var culture = new System.Globalization.CultureInfo(lang);
var strings = endpoint.SelectMany(x => factory.Create(x.TranslationsBaseName, x.TranslationsLocation).GetAllStrings(culture, includeParentCultures: true));
return TypedResults.Ok(strings.ToObjectGraph());
});
}
return routes;
}
}
Expand All @@ -57,20 +63,54 @@ public static IEndpointRouteBuilder MapTranslationGraph(this IEndpointRouteBuild
/// </summary>
public class TranslationsGraphOptions
{
/// <summary>
/// Additional endpoints/resources
/// </summary>
internal List<TranslationGraphResource> Resources { get; } = [];

/// <summary>
/// A dot dlimited path to the folder containing the Resex file with the translations key values. Defaults to <strong>"Resources.UiTranslations"</strong>
/// </summary>
public string TranslationsBaseName { get; set; } = "Resources.UiTranslations";
public string DefaultTranslationsBaseName { get; set; } = "Resources.UiTranslations";

/// <summary>
/// The assembly name containing the translation resex files as embeded resources. Defaults to <strong>Assembly.GetEntryAssembly()!.GetName().Name!</strong>
/// </summary>
public string TranslationsLocation { get; set; } = Assembly.GetEntryAssembly()!.GetName().Name!;
public string DefaultTranslationsLocation { get; set; } = Assembly.GetEntryAssembly()!.GetName().Name!;
/// <summary>
/// The endpoint route pattern defaults to <strong>"/translations.{lang:culture}.json"</strong>. If changes are made to the path we must paintain the lang parameter.
/// </summary>
[StringSyntax("Route")]
public string EndpointRoutePattern { get; set; } = "/translations.{lang:culture}.json";
public string DefaultEndpointRoutePattern { get; set; } = "/translations.{lang:culture}.json";

/// <summary>
/// Encapsulates the settings needed to run an enpoint
/// </summary>
public record TranslationGraphResource([StringSyntax("Route")] string EndpointRoutePattern, string TranslationsBaseName, string TranslationsLocation);

/// <summary>
/// adds additional endpoints/resources. Appart form the default settings
/// </summary>
/// <param name="translationsBaseName">A dot dlimited path to the folder containing the Resex file with the translations key values. For example <strong>Resources.UiTranslations</strong></param>
/// <param name="endpointRoutePattern">The endpoint route pattern. If changes are made to the path we must paintain the <strong>{lang}</strong> parameter. Defaults to <strong>"/translations.{lang:culture}.json"</strong></param>
/// <param name="translationsLocation">The assembly name containing the translation resex files as embeded resources. Defaults to <strong>Assembly.GetEntryAssembly()!.GetName().Name!</strong></param>
/// <returns></returns>
public TranslationsGraphOptions AddResource(string translationsBaseName, [StringSyntax("Route")] string? endpointRoutePattern = null, string? translationsLocation = null) {
var resource = new TranslationGraphResource(endpointRoutePattern ?? DefaultEndpointRoutePattern, translationsBaseName, translationsLocation ?? DefaultTranslationsLocation);
Resources.Add(resource);
return this;
}

/// <summary>
/// Gets all available endpoint configurations groupd by endpoint route pattern in order to configure aspnet core endpoint routing.
/// </summary>
/// <returns></returns>
public ILookup<string, TranslationGraphResource> GetEndpoints() {
List<TranslationGraphResource> all = [new (DefaultEndpointRoutePattern, DefaultTranslationsBaseName, DefaultTranslationsLocation), ..Resources];
return all.ToLookup(x => x.EndpointRoutePattern);
}
}


#nullable disable
#endif
8 changes: 4 additions & 4 deletions src/Indice.Common/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,14 @@ public static string ToKebabCase(this string value) {

#if NET7_0_OR_GREATER
/// <summary>Match all parts of a sentence that start with one capital letter e.g. Net</summary>
[GeneratedRegex(@"(?<delimiter>[/\\])?(?<word>[A-Z]?[a-z0-9]+)[,;|\s]*")]
[GeneratedRegex(@"(?<delimiter>[/\\-])?(?<word>[A-Z]?[a-z0-9.]+)[,;|\s]*")]
private static partial Regex WordsRegex();
/// <summary>Match all parts of a sentence that are all capital letter e.g. NET</summary>
[GeneratedRegex(@"(?<delimiter>[/\\])?(?<word>[A-Z][A-Z0-9]*)[,;|\s]*")]
[GeneratedRegex(@"(?<delimiter>[/\\-])?(?<word>[A-Z][A-Z0-9.]*)[,;|\s]*")]
private static partial Regex WordsAllCapsRegex();
#else
private static readonly Regex _wordsRegex = new(@"(?<delimiter>[/\\])?(?<word>[A-Z]?[a-z0-9]+)[,;|\s]*");
private static readonly Regex _wordsAllCapsRegex = new(@"(?<delimiter>[/\\])?(?<word>[A-Z][A-Z0-9]*)[,;|\s]");
private static readonly Regex _wordsRegex = new(@"(?<delimiter>[/\\-])?(?<word>[A-Z]?[a-z0-9.]+)[,;|\s]*");
private static readonly Regex _wordsAllCapsRegex = new(@"(?<delimiter>[/\\-])?(?<word>[A-Z][A-Z0-9.]*)[,;|\s]*");
/// <summary>Match all parts of a sentence that start with one capital letter e.g. Net</summary>
private static Regex WordsRegex() => _wordsRegex;
/// <summary>Match all parts of a sentence that are all capital letter e.g. NET</summary>
Expand Down
15 changes: 13 additions & 2 deletions src/Indice.Common/Localization/IStringLocalizerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,16 @@ public static IEnumerable<LocalizedString> GetAllStrings(this IStringLocalizer s
/// <param name="pathDelimiter">The delimiter character for determining a path from a given key. For example for key &quot;feature.dashboard.title&quot; would be '.' </param>
/// <returns>The object graph in the form of a <em>Dictionary&lt;string, object&gt;</em></returns>
public static Dictionary<string, object> ToObjectGraph(this IStringLocalizer stringLocalizer, CultureInfo culture, char pathDelimiter = '.') =>
stringLocalizer.GetAllStrings(culture, includeParentCultures: true).ToObjectGraphInternal(pathDelimiter);
stringLocalizer.GetAllStrings(culture, includeParentCultures: true).ToObjectGraph(pathDelimiter);

internal static Dictionary<string, object> ToObjectGraphInternal(this IEnumerable<LocalizedString> strings, char pathDelimiter = '.') {
/// <summary>
/// Creates an object graph of nested <em>Dictionary&lt;string, object&gt;</em>
/// equivalent to the flat structure represented by the keys of the resource strings inside the <paramref name="strings"/>
/// </summary>
/// <param name="strings">The source strings</param>
/// <param name="pathDelimiter">The delimiter character for determining a path from a given key. For example for key &quot;feature.dashboard.title&quot; would be '.'</param>
/// <returns></returns>
public static Dictionary<string, object> ToObjectGraph(this IEnumerable<LocalizedString> strings, char pathDelimiter = '.') {
var graph = new Dictionary<string, object>();
foreach (var keyValue in strings) {
var keyPath = keyValue.Name.Split(pathDelimiter)!;
Expand All @@ -56,6 +63,10 @@ internal static Dictionary<string, object> ToObjectGraphInternal(this IEnumerabl
node = (Dictionary<string, object>)node[subKey];
} else {
var value = keyValue.Value;
if (node.ContainsKey(keyPath[depth])) {
node[keyPath[depth]] = value;
break;
}
node.Add(keyPath[depth], value);
break;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public static class ResourceManagerExtensions
public static Dictionary<string, object> ToObjectGraph(this System.Resources.ResourceManager resourceManager, CultureInfo? cultureInfo = null, char pathDelimiter = '.') {
var set = resourceManager.GetResourceSet(cultureInfo ?? CultureInfo.CurrentUICulture, createIfNotExists: true, tryParents: true)!;
var strings = set.Cast<DictionaryEntry>().Select(x => new LocalizedString(x.Key.ToString()!, set.GetString(x.Key.ToString()!)!));
return IStringLocalizerExtensions.ToObjectGraphInternal(strings, pathDelimiter);
return IStringLocalizerExtensions.ToObjectGraph(strings, pathDelimiter);
}
}
#nullable disable
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ public async Task Delete(Guid caseTypeId) {
public async Task<CaseType> GetCaseTypeDetailsById(Guid id) {
var dbCaseType = await _dbContext.CaseTypes
.AsNoTracking()
.Include(x => x.CheckpointTypes)
.FirstOrDefaultAsync(p => p.Id == id);

var caseTypeRoles = await _dbContext.Members.AsQueryable()
Expand Down
13 changes: 13 additions & 0 deletions src/Indice.Features.Identity.Core/Events/UserUnBlockedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Indice.Events;
using Indice.Features.Identity.Core.Events.Models;

namespace Indice.Features.Identity.Core.Events;

/// <summary>An event that is raised when a user is blocked.</summary>
/// <remarks>Creates a new instance of <see cref="UserBlockedEvent"/>.</remarks>
/// <param name="user">The user context.</param>
public class UserUnBlockedEvent(UserEventContext user) : IPlatformEvent
{
/// <summary>The user context.</summary>
public UserEventContext User { get; } = user;
}
5 changes: 5 additions & 0 deletions src/Indice.Features.Identity.Core/ExtendedUserManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -355,12 +355,17 @@ public async Task<IdentityResult> SetBlockedAsync(TUser user, bool blocked, Canc
if (user is null) {
throw new ArgumentNullException(nameof(user));
}
var changed = user.Blocked != blocked;
user.Blocked = blocked;
var result = await UpdateAsync(user);
if (result.Succeeded && blocked) {
// When blocking a user we need to make sure we also revoke all of his tokens.
await _eventService.Publish(new UserBlockedEvent(UserEventContext.InitializeFromUser(user)));
}
if (result.Succeeded && !blocked) {
// When un-blocking a notify whomever is interested.
await _eventService.Publish(new UserUnBlockedEvent(UserEventContext.InitializeFromUser(user)));
}
return result;
}
#endregion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,12 @@ internal static async Task<Results<Ok<MediaFile>, NotFound>> GetFileDetails(Guid
return TypedResults.Ok(file);
}

internal static async Task<CreatedAtRoute<UploadFileResponse>> UploadFile(UploadFileRequest request, MediaManager mediaManager) {
var fileId = await mediaManager.UploadFile(request.ToUploadFileCommand());
return TypedResults.CreatedAtRoute(new UploadFileResponse(fileId), nameof(GetFileDetails), new { fileId });
internal static async Task<Results<Ok<UploadFileResponse>, CreatedAtRoute<UploadFileResponse>>> UploadFile(UploadFileRequest request, MediaManager mediaManager) {
var fileIds = await mediaManager.UploadFiles(request.ToUploadFileCommand());
if (fileIds.Count > 1) {
TypedResults.Ok(new UploadFileResponse(fileIds.ToArray()));
}
return TypedResults.CreatedAtRoute(new UploadFileResponse(fileIds.ToArray()), nameof(GetFileDetails), new { fileId = fileIds[0] });
}

internal static async Task<NoContent> UpdateFileMetadata(Guid fileId, UpdateFileMetadataRequest request, MediaManager mediaManager) {
Expand Down
25 changes: 9 additions & 16 deletions src/Indice.Features.Media.AspNetCore/Mappings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,23 +46,16 @@ internal static void Update(this DbMediaFolder dbFolder, UpdateFolderCommand com
};
#endregion
#region File
internal static UploadFileCommand ToUploadFileCommand(this UploadFileRequest request) {
if (request.File is null) {
throw new ArgumentOutOfRangeException(nameof(request), "property File is empty.");
internal static List<UploadFileCommand> ToUploadFileCommand(this UploadFileRequest request) {
if (request.Files is null) {
throw new ArgumentOutOfRangeException(nameof(request), "property Files is empty.");
}
return new (
request.File.OpenReadStream,
fileName: request.File.FileName,
contentLength: (int)request.File.Length,
contentType: request.File.ContentType) {
FolderId = request.FolderId
};
//if (File.Length > 0) {
// using var inputStream = File.OpenReadStream();
// using var memoryStream = new MemoryStream();
// inputStream.CopyTo(memoryStream);
// fileParameter.Data = memoryStream.ToArray();
//}
return request.Files.Select(file => new UploadFileCommand(
() => file.OpenReadStream(),
fileName: file.FileName,
contentLength: (int)file.Length,
contentType: file.ContentType) { FolderId = request.FolderId }
).ToList();
}
internal static UpdateFileMetadataCommand ToUpdateFileMetadataCommand(this UpdateFileMetadataRequest request, Guid id) => new() {
Id = id,
Expand Down
23 changes: 23 additions & 0 deletions src/Indice.Features.Media.AspNetCore/MediaManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
using Microsoft.Extensions.Configuration;
using SixLabors.ImageSharp;
using Indice.Features.Media.AspNetCore.Data.Models;
using static Microsoft.EntityFrameworkCore.DbLoggerCategory.Database;

namespace Indice.Features.Media.AspNetCore;
/// <summary>A manager class that helps work with the Media Library API infrastructure.</summary>
Expand Down Expand Up @@ -192,6 +193,28 @@ public async Task<Guid> UploadFile(UploadFileCommand command) {
return fileId;
}

/// <summary>Uploads a file in the system.</summary>
/// <param name="commands">The command containing all the required data to upload a file.</param>
public async Task<List<Guid>> UploadFiles(List<UploadFileCommand> commands) {
var dbFiles = commands.Select(Mapper.ToDbFile).ToList();
var fileIds = await _fileStore.CreateMany(dbFiles);
var cacheKey = $"{CONTENT_CACHE_KEY}_";
var uploadTasks = new List<Task>();
for (var i = 0; i < commands.Count; i++) {
var save = async () => {
using (var stream = commands[i].OpenReadStream()) {
await _fileService.SaveAsync($"media/{dbFiles[i].Path.TrimStart('/')}", stream);
}
};
uploadTasks.Add(save());
cacheKey = $"{CONTENT_CACHE_KEY}_{(commands[i].FolderId.HasValue ? commands[i].FolderId!.Value : "root")}";
}
await Task.WhenAll(uploadTasks);
await _cache.RemoveAsync(cacheKey);
await _cache.RemoveAsync(STRUCT_CACHE_KEY);
return fileIds;
}

/// <summary>Updates metadata about a file.</summary>
/// <param name="command">The command containing all the required data to update a file.</param>
public async Task UpdateFileMetadata(UpdateFileMetadataCommand command) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public class UploadFileRequest
public Guid? FolderId { get; set; }
/// <summary>The file to be uploaded.</summary>
[Required]
public IFormFile? File { get; set; }
public IFormFileCollection? Files { get; set; }

/// <summary> Binds the File from Form to Request property. </summary>
/// <param name="context">The <see cref="HttpContext"/>.</param>
Expand All @@ -28,7 +28,7 @@ public static async ValueTask<UploadFileRequest> BindAsync(HttpContext context,
folderId = folderIdValue;
}
return new UploadFileRequest {
File = files[0],
Files = files,
FolderId = folderId
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
/// <summary>
/// Represents the The folder created response with the folder Id.
/// </summary>
/// <param name="FileId">The file Id for the media file created</param>
public record UploadFileResponse(Guid FileId);
/// <param name="FileIds">The file Id for the media file created</param>
public record UploadFileResponse(Guid[] FileIds);
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public interface IMediaFileStore
/// <summary>Creates a new file.</summary>
/// <param name="file">The file.</param>
Task<Guid> Create(DbMediaFile file);
/// <summary>Bulk create new files.</summary>
/// <param name="files">The files.</param>
Task<List<Guid>> CreateMany(List<DbMediaFile> files);
/// <summary>Updates an existing file.</summary>
/// <param name="file">The file.</param>
Task Update(DbMediaFile file);
Expand Down
12 changes: 12 additions & 0 deletions src/Indice.Features.Media.AspNetCore/Stores/MediaFileStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ public async Task<Guid> Create(DbMediaFile file) {
await _dbContext.SaveChangesAsync();
return file.Id;
}

public async Task<List<Guid>> CreateMany(List<DbMediaFile> files) {
var folderid = files.FirstOrDefault()?.FolderId;
var paths = await MediaFolderStore.FindPathsAsync(_dbContext, folderid, files.Select(x => x.Name).ToArray());
for (var i = 0; i < paths.Length; i++) {
files[i].Path = paths[i];
}
_dbContext.Files.AddRange(files);
await _dbContext.SaveChangesAsync();
return files.Select(x => x.Id).ToList();
}

/// <inheritdoc/>
public async Task Update(DbMediaFile file) {
file.Path = await MediaFolderStore.FindPathAsync(_dbContext, file.FolderId, file.Name);
Expand Down
Loading

0 comments on commit 8ab35e0

Please sign in to comment.