Merge branch 'feature/admincontext-dependencyinjection' into 'dev'

Resolve "Proper Admin Context dependency injection"

Closes #21

See merge request leon.hoppe/HopFrame!11
This commit is contained in:
2024-11-22 17:57:39 +00:00
14 changed files with 136 additions and 74 deletions

View File

@@ -25,6 +25,8 @@ simply by reading the structure of the provided model and optionally some additi
} }
``` ```
> **Hint:** you can specify the url of the admin page by adding the `AdminPageUrl` Attribute
3. **Optionally** you can further configure your pages in the `OnModelCreating` method 3. **Optionally** you can further configure your pages in the `OnModelCreating` method
```csharp ```csharp
@@ -61,6 +63,15 @@ simply by reading the structure of the provided model and optionally some additi
``` ```
4. **Optionally** you can also add some of the following attributes to your classes / properties to further configure the admin pages:\ 4. **Optionally** you can also add some of the following attributes to your classes / properties to further configure the admin pages:\
\ \
Attributes for classes and properties:
```csharp
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)]
public sealed class AdminNameAttribute(string name) : Attribute {
public string Name { get; set; } = name;
}
```
Attributes for classes: Attributes for classes:
```csharp ```csharp
@@ -86,8 +97,8 @@ simply by reading the structure of the provided model and optionally some additi
```csharp ```csharp
[AttributeUsage(AttributeTargets.Class)] [AttributeUsage(AttributeTargets.Class)]
public class AdminUrlAttribute(string url) : Attribute { public sealed class AdminDescriptionAttribute(string description) : Attribute {
public string Url { get; set; } = url; public string Description { get; set; } = description;
} }
``` ```

View File

@@ -0,0 +1,10 @@
namespace HopFrame.Web.Admin.Attributes;
/// <summary>
/// This attribute specifies the url of the admin page and needs to be applied on the AdminPage property in the AdminContext directly
/// </summary>
/// <param name="url">The page url: '/administration/{url}'</param>
[AttributeUsage(AttributeTargets.Property)]
public sealed class AdminPageUrlAttribute(string url) : Attribute {
public string Url { get; set; } = url;
}

View File

@@ -1,6 +1,6 @@
namespace HopFrame.Web.Admin.Attributes; namespace HopFrame.Web.Admin.Attributes;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] [AttributeUsage(AttributeTargets.Class)]
public sealed class AdminDescriptionAttribute(string description) : Attribute { public sealed class AdminDescriptionAttribute(string description) : Attribute {
public string Description { get; set; } = description; public string Description { get; set; } = description;
} }

View File

@@ -1,6 +0,0 @@
namespace HopFrame.Web.Admin.Attributes.Classes;
[AttributeUsage(AttributeTargets.Class)]
public class AdminUrlAttribute(string url) : Attribute {
public string Url { get; set; } = url;
}

View File

@@ -19,13 +19,6 @@ public interface IAdminPageGenerator<TModel> {
/// <returns></returns> /// <returns></returns>
IAdminPageGenerator<TModel> Description(string description); IAdminPageGenerator<TModel> Description(string description);
/// <summary>
/// Sets the url for the Admin Page
/// </summary>
/// <param name="url">the specified url (administration/{url})</param>
/// <returns></returns>
IAdminPageGenerator<TModel> Url(string url);
/// <summary> /// <summary>
/// Sets the permission needed to view the Admin Page /// Sets the permission needed to view the Admin Page
/// </summary> /// </summary>

View File

@@ -1,6 +1,5 @@
using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Models; using HopFrame.Web.Admin.Models;
using HopFrame.Web.Admin.Providers;
using Microsoft.Extensions.DependencyInjection;
namespace HopFrame.Web.Admin.Generators.Implementation; namespace HopFrame.Web.Admin.Generators.Implementation;
@@ -27,13 +26,14 @@ internal class AdminContextGenerator : IAdminContextGenerator {
return (generator as AdminPageGenerator<TModel>)?.Compile(); return (generator as AdminPageGenerator<TModel>)?.Compile();
} }
public TContext CompileContext<TContext>() where TContext : AdminPagesContext { public TContext CompileContext<TContext>(IServiceProvider provider) where TContext : AdminPagesContext {
var type = typeof(TContext); var type = typeof(TContext);
var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage)); var compileMethod = typeof(AdminContextGenerator).GetMethod(nameof(CompilePage));
var properties = type.GetProperties(); var properties = type.GetProperties();
var context = Activator.CreateInstance<TContext>(); var dependencies = ResolveDependencies<TContext>(provider);
var context = Activator.CreateInstance(type, dependencies) as TContext;
foreach (var property in properties) { foreach (var property in properties) {
var propertyType = property.PropertyType.GenericTypeArguments[0]; var propertyType = property.PropertyType.GenericTypeArguments[0];
@@ -49,31 +49,48 @@ internal class AdminContextGenerator : IAdminContextGenerator {
_adminPages.Add(propertyType, generatorInstance); _adminPages.Add(propertyType, generatorInstance);
} }
context.OnModelCreating(this); context?.OnModelCreating(this);
foreach (var property in properties) { foreach (var property in properties) {
var modelType = property.PropertyType.GenericTypeArguments[0]; var modelType = property.PropertyType.GenericTypeArguments[0];
var method = compileMethod?.MakeGenericMethod(modelType); var method = compileMethod?.MakeGenericMethod(modelType);
property.SetValue(context, method?.Invoke(this, [])); var compiledPage = method?.Invoke(this, []) as AdminPage;
var url = property.Name;
if (property.GetCustomAttributes(false).Any(a => a is AdminPageUrlAttribute)) {
var attribute = property.GetCustomAttributes(false)
.Single(a => a is AdminPageUrlAttribute) as AdminPageUrlAttribute;
url = attribute?.Url;
}
compiledPage!.Url = url;
property.SetValue(context, compiledPage);
} }
return context; return context;
} }
private object[] ResolveDependencies<TContext>(IServiceProvider provider) {
return ResolveDependencies(typeof(TContext), provider);
public static void RegisterPages(AdminPagesContext context, IAdminPagesProvider provider, IServiceCollection services) {
var properties = context.GetType().GetProperties();
foreach (var property in properties) {
var page = property.GetValue(context) as AdminPage;
if (page is null) continue;
provider.RegisterAdminPage(page.Title.ToLower(), page);
if (page.RepositoryProvider is not null)
services.AddScoped(page.RepositoryProvider);
} }
public static object[] ResolveDependencies(Type type, IServiceProvider provider) {
var ctors = type.GetConstructors();
if (ctors.Length == 0) return [];
if (ctors.Length > 1)
throw new ArgumentException($"Dependencies of {type.Name} could not be resolved (multiple constructors)!");
var ctor = ctors[0];
var depTypes = ctor.GetParameters();
var dependencies = new object[depTypes.Length];
for (var i = 0; i < depTypes.Length; i++) {
dependencies[i] = provider.GetService(depTypes[i].ParameterType);
}
return dependencies;
} }
} }

View File

@@ -40,7 +40,6 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
public IAdminPageGenerator<TModel> Title(string title) { public IAdminPageGenerator<TModel> Title(string title) {
Page.Title = title; Page.Title = title;
Page.Url ??= title.ToLower();
return this; return this;
} }
@@ -49,11 +48,6 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
return this; return this;
} }
public IAdminPageGenerator<TModel> Url(string url) {
Page.Url = url;
return this;
}
public IAdminPageGenerator<TModel> ViewPermission(string permission) { public IAdminPageGenerator<TModel> ViewPermission(string permission) {
Page.Permissions.View = permission; Page.Permissions.View = permission;
return this; return this;
@@ -167,11 +161,6 @@ internal sealed class AdminPageGenerator<TModel> : IAdminPageGenerator<TModel>,
Description(attribute?.Description); Description(attribute?.Description);
} }
if (attributes.Any(a => a is AdminUrlAttribute)) {
var attribute = attributes.Single(a => a is AdminUrlAttribute) as AdminUrlAttribute;
Url(attribute?.Url);
}
if (attributes.Any(a => a is AdminPermissionsAttribute)) { if (attributes.Any(a => a is AdminPermissionsAttribute)) {
var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute; var attribute = attributes.Single(a => a is AdminPermissionsAttribute) as AdminPermissionsAttribute;
CreatePermission(attribute?.Permissions.Create); CreatePermission(attribute?.Permissions.Create);

View File

@@ -1,5 +1,5 @@
using System.ComponentModel; using System.ComponentModel;
using System.Text.Json.Serialization; using HopFrame.Web.Admin.Generators.Implementation;
namespace HopFrame.Web.Admin.Models; namespace HopFrame.Web.Admin.Models;
@@ -23,4 +23,11 @@ public class AdminPage {
public bool ShowCreateButton { get; set; } = true; public bool ShowCreateButton { get; set; } = true;
public bool ShowDeleteButton { get; set; } = true; public bool ShowDeleteButton { get; set; } = true;
public bool ShowUpdateButton { get; set; } = true; public bool ShowUpdateButton { get; set; } = true;
public IModelRepository LoadModelRepository(IServiceProvider provider) {
if (RepositoryProvider is null) return null;
var dependencies = AdminContextGenerator.ResolveDependencies(RepositoryProvider, provider);
return Activator.CreateInstance(RepositoryProvider, dependencies) as IModelRepository;
}
} }

View File

@@ -4,7 +4,6 @@ namespace HopFrame.Web.Admin.Providers;
public interface IAdminPagesProvider { public interface IAdminPagesProvider {
internal void RegisterAdminPage(string url, AdminPage page);
AdminPage LoadAdminPage(string url); AdminPage LoadAdminPage(string url);
IList<AdminPage> LoadRegisteredAdminPages(); IList<AdminPage> LoadRegisteredAdminPages();
AdminPage HasPageFor(Type type); AdminPage HasPageFor(Type type);

View File

@@ -2,25 +2,44 @@ using HopFrame.Web.Admin.Models;
namespace HopFrame.Web.Admin.Providers.Implementation; namespace HopFrame.Web.Admin.Providers.Implementation;
public class AdminPagesProvider : IAdminPagesProvider { public class AdminPagesProvider(IServiceProvider provider) : IAdminPagesProvider {
private readonly IDictionary<string, AdminPage> _pages = new Dictionary<string, AdminPage>(); private static readonly IDictionary<string, PageDataStore> Pages = new Dictionary<string, PageDataStore>();
public void RegisterAdminPage(string url, AdminPage page) { public static void RegisterAdminPage<TContext>(string url, Type pageType) where TContext : AdminPagesContext {
_pages.Add(url, page); Pages.Add(url, new PageDataStore {
ContextType = typeof(TContext),
PageType = pageType
});
} }
public AdminPage LoadAdminPage(string url) { public AdminPage LoadAdminPage(string url) {
return _pages.TryGetValue(url, out var page) ? page : null; if (!Pages.TryGetValue(url, out var data)) return null;
var context = provider.GetService(data.ContextType);
var property = data.ContextType.GetProperties()
.SingleOrDefault(prop => prop.PropertyType == data.PageType);
return property?.GetValue(context) as AdminPage;
} }
public IList<AdminPage> LoadRegisteredAdminPages() { public IList<AdminPage> LoadRegisteredAdminPages() {
return _pages.Values.ToList(); return Pages
.Select(pair => LoadAdminPage(pair.Key))
.ToList();
} }
public AdminPage HasPageFor(Type type) { public AdminPage HasPageFor(Type type) {
return _pages foreach (var (url, data) in Pages) {
.Where(p => p.Value.ModelType == type) var innerType = data.PageType.GenericTypeArguments[0];
.Select(p => p.Value) if (innerType != type) continue;
.SingleOrDefault(); return LoadAdminPage(url);
}
return null;
} }
} }
internal struct PageDataStore {
public Type PageType { get; set; }
public Type ContextType { get; set; }
}

View File

@@ -1,3 +1,4 @@
using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Generators.Implementation; using HopFrame.Web.Admin.Generators.Implementation;
using HopFrame.Web.Admin.Providers; using HopFrame.Web.Admin.Providers;
using HopFrame.Web.Admin.Providers.Implementation; using HopFrame.Web.Admin.Providers.Implementation;
@@ -8,22 +9,36 @@ namespace HopFrame.Web.Admin;
public static class ServiceCollectionExtensions { public static class ServiceCollectionExtensions {
private static IAdminPagesProvider _provider;
public static IServiceCollection AddAdminContext<TContext>(this IServiceCollection services) where TContext : AdminPagesContext { public static IServiceCollection AddAdminContext<TContext>(this IServiceCollection services) where TContext : AdminPagesContext {
var provider = GetProvider(); services.TryAddSingleton<IAdminPagesProvider, AdminPagesProvider>();
services.TryAddSingleton(provider);
services.AddSingleton(provider => {
var generator = new AdminContextGenerator(); var generator = new AdminContextGenerator();
var context = generator.CompileContext<TContext>(); var context = generator.CompileContext<TContext>(provider);
AdminContextGenerator.RegisterPages(context, provider, services); return context;
services.AddSingleton(context); });
PreregisterPages<TContext>();
return services; return services;
} }
private static IAdminPagesProvider GetProvider() { private static void PreregisterPages<TContext>() where TContext : AdminPagesContext {
return _provider ??= new AdminPagesProvider(); var contextType = typeof(TContext);
var props = contextType.GetProperties();
foreach (var property in props) {
var url = property.Name;
if (property.GetCustomAttributes(false).Any(a => a is AdminPageUrlAttribute)) {
var attribute = property.GetCustomAttributes(false)
.Single(a => a is AdminPageUrlAttribute) as AdminPageUrlAttribute;
url = attribute?.Url;
}
AdminPagesProvider.RegisterAdminPage<TContext>(url, property.PropertyType);
}
} }
} }

View File

@@ -1,7 +1,6 @@
@rendermode InteractiveServer @rendermode InteractiveServer
@using System.Collections @using System.Collections
@using System.Globalization
@using BlazorStrap @using BlazorStrap
@using BlazorStrap.Shared.Components.Modal @using BlazorStrap.Shared.Components.Modal
@using static Microsoft.AspNetCore.Components.Web.RenderMode @using static Microsoft.AspNetCore.Components.Web.RenderMode
@@ -144,7 +143,7 @@
_currentPage = page; _currentPage = page;
_entry = entryToEdit; _entry = entryToEdit;
_isEdit = entryToEdit is not null; _isEdit = entryToEdit is not null;
_repository = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository; _repository = _currentPage.LoadModelRepository(Provider);
_entry ??= Activator.CreateInstance(_currentPage.ModelType); _entry ??= Activator.CreateInstance(_currentPage.ModelType);
_context = new EditContext(_entry); _context = new EditContext(_entry);
@@ -247,8 +246,7 @@
foreach (var value in _values) { foreach (var value in _values) {
if (value.Key.Unique) { if (value.Key.Unique) {
if (value.Value == value.Key.GetValue(_entry)) continue; if (value.Value == value.Key.GetValue(_entry)) continue;
var repo = Provider.GetService(_currentPage.RepositoryProvider) as IModelRepository; var data = _repository!.ReadAllO().GetAwaiter().GetResult();
var data = repo!.ReadAllO().GetAwaiter().GetResult();
foreach (var entry in data) { foreach (var entry in data) {
var other = value.Key.GetValue(entry); var other = value.Key.GetValue(entry);
if (!other.Equals(value.Value)) continue; if (!other.Equals(value.Value)) continue;
@@ -295,7 +293,7 @@
throw new ArgumentException($"'{property.Name}' cannot be a selector because a admin page for '{type.Name}' does not exist!"); throw new ArgumentException($"'{property.Name}' cannot be a selector because a admin page for '{type.Name}' does not exist!");
} }
var repo = Provider.GetService(page.RepositoryProvider) as IModelRepository; var repo = page.LoadModelRepository(Provider);
var objects = (await repo!.ReadAllO()).ToArray(); var objects = (await repo!.ReadAllO()).ToArray();
_selectorValues[property] = objects; _selectorValues[property] = objects;

View File

@@ -2,6 +2,7 @@ using System.Text.RegularExpressions;
using HopFrame.Database.Models; using HopFrame.Database.Models;
using HopFrame.Security; using HopFrame.Security;
using HopFrame.Web.Admin; using HopFrame.Web.Admin;
using HopFrame.Web.Admin.Attributes;
using HopFrame.Web.Admin.Generators; using HopFrame.Web.Admin.Generators;
using HopFrame.Web.Admin.Models; using HopFrame.Web.Admin.Models;
using HopFrame.Web.Repositories; using HopFrame.Web.Repositories;
@@ -10,7 +11,10 @@ namespace HopFrame.Web;
internal class HopAdminContext : AdminPagesContext { internal class HopAdminContext : AdminPagesContext {
[AdminPageUrl("users")]
public AdminPage<User> Users { get; set; } public AdminPage<User> Users { get; set; }
[AdminPageUrl("groups")]
public AdminPage<PermissionGroup> Groups { get; set; } public AdminPage<PermissionGroup> Groups { get; set; }
public override void OnModelCreating(IAdminContextGenerator generator) { public override void OnModelCreating(IAdminContextGenerator generator) {

View File

@@ -106,6 +106,7 @@
@inject ITokenContext Auth @inject ITokenContext Auth
@inject IPermissionRepository Permissions @inject IPermissionRepository Permissions
@inject SweetAlertService Alerts @inject SweetAlertService Alerts
@inject NavigationManager Navigator
@code { @code {
[Parameter] [Parameter]
@@ -127,12 +128,17 @@
protected override async Task OnInitializedAsync() { protected override async Task OnInitializedAsync() {
_pageData = Pages.LoadAdminPage(Url); _pageData = Pages.LoadAdminPage(Url);
if (_pageData is null) {
Navigator.NavigateTo("/administration", true);
return;
}
_currentSortProperty = _pageData.DefaultSortPropertyName; _currentSortProperty = _pageData.DefaultSortPropertyName;
_currentSortDirection = _pageData.DefaultSortDirection; _currentSortDirection = _pageData.DefaultSortDirection;
if (_pageData.RepositoryProvider is null) if (_pageData.RepositoryProvider is null)
throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'"); throw new ArgumentException($"AdminPage '{_pageData.Title}' does not specify a model repository!'");
_modelRepository = Provider.GetService(_pageData.RepositoryProvider) as IModelRepository; _modelRepository = _pageData.LoadModelRepository(Provider);
_hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update); _hasEditPermission = _pageData.Permissions.Update is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Update);
_hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete); _hasDeletePermission = _pageData.Permissions.Delete is null || await Permissions.HasPermission(Auth.User, _pageData.Permissions.Delete);