Skip to content

Commit

Permalink
Some updated to mudblazor crud components and ModelBase.
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix-CodingClimber committed Feb 8, 2024
1 parent 5b2145c commit 3d63a85
Show file tree
Hide file tree
Showing 11 changed files with 302 additions and 43 deletions.
2 changes: 1 addition & 1 deletion src/DotNetElements.Core/Core/ModelBase.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
namespace DotNetElements.Core;

public interface IModel<TKey> : IHasKey<TKey>
where TKey : notnull, IEquatable<TKey>;
where TKey : notnull, IEquatable<TKey>;

public interface IEditModel<TModel, TKey>
where TModel : Model<TKey>
Expand Down
12 changes: 12 additions & 0 deletions src/DotNetElements.Core/Core/RelatedEntitiesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,15 @@ public RelatedEntitiesAttribute(string[] referenceProperties)

}
}

[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class RelatedEntitiesCollectionsAttribute : Attribute
{
public string[] ReferenceProperties { get; private init; }

public RelatedEntitiesCollectionsAttribute(string[] referenceProperties)
{
ReferenceProperties = referenceProperties;

}
}
47 changes: 29 additions & 18 deletions src/DotNetElements.Core/Core/Repository.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.EntityFrameworkCore.ChangeTracking;

namespace DotNetElements.Core;

Expand All @@ -12,6 +13,7 @@ public abstract class Repository<TDbContext, TEntity, TKey> : ReadOnlyRepository
protected TimeProvider TimeProvider { get; private init; }

protected static readonly RelatedEntitiesAttribute? RelatedEntities = typeof(TEntity).GetCustomAttribute<RelatedEntitiesAttribute>();
protected static readonly RelatedEntitiesCollectionsAttribute? RelatedEntitiesCollections = typeof(TEntity).GetCustomAttribute<RelatedEntitiesCollectionsAttribute>();

public Repository(TDbContext dbContext, ICurrentUserProvider currentUserProvider, TimeProvider timeProvider) : base(dbContext)
{
Expand All @@ -35,18 +37,11 @@ public virtual async Task<CrudResult<TEntity>> CreateAsync(TEntity entity, Expre
if (entity is ICreationAuditedEntity<TKey> auditedEntity)
auditedEntity.SetCreationAudited(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());

var createdEntity = Entities.Attach(entity);
EntityEntry<TEntity> createdEntity = Entities.Attach(entity);

await DbContext.SaveChangesAsync();

// todo, review this part. Maybe add parameter returnRelatedEntities?
// (Was needed, so referenced entities got included when returning entity)
// Check if we can batch the loadings
if (RelatedEntities is not null)
{
foreach (string relatedProperty in RelatedEntities.ReferenceProperties)
await createdEntity.Reference(relatedProperty).LoadAsync();
}
await LoadRelatedEntities(createdEntity);

return createdEntity.Entity;
}
Expand All @@ -71,7 +66,7 @@ public virtual async Task<CrudResult<TSelf>> CreateOrUpdateAsync<TSelf>(TKey id,
if (entity is ICreationAuditedEntity<TKey> auditedEntity)
auditedEntity.SetCreationAudited(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());

var createdEntity = DbContext.Set<TSelf>().Attach(entity);
EntityEntry<TSelf> createdEntity = DbContext.Set<TSelf>().Attach(entity);

await DbContext.SaveChangesAsync();

Expand Down Expand Up @@ -130,14 +125,7 @@ public virtual async Task<CrudResult<TEntity>> UpdateAsync<TFrom>(TKey id, TFrom
return CrudResult.ConcurrencyConflict();
}

// todo, review this part. Maybe add parameter returnRelatedEntities?
// (Was needed, so referenced entities got updated/included)
// Check if we can batch the loadings
if (RelatedEntities is not null)
{
foreach (string relatedProperty in RelatedEntities.ReferenceProperties)
await DbContext.Entry(existingEntity).Reference(relatedProperty).LoadAsync();
}
await LoadRelatedEntities(existingEntity);

return CrudResult.OkIfNotNull(existingEntity, CrudError.Unknown);
}
Expand Down Expand Up @@ -258,4 +246,27 @@ protected async Task<bool> SaveChangesWithVersionCheckAsync()

return true;
}

private Task LoadRelatedEntities(TEntity entity)
{
return LoadRelatedEntities(DbContext.Entry(entity));
}

// todo, review loading related entities. Maybe add parameter returnRelatedEntities?
// (Was needed, so referenced entities got included when returning entity)
// Check if we can batch the loadings
private static async Task LoadRelatedEntities(EntityEntry<TEntity> entity)
{
if (RelatedEntities is not null)
{
foreach (string relatedProperty in RelatedEntities.ReferenceProperties)
await entity.Reference(relatedProperty).LoadAsync();
}

if (RelatedEntitiesCollections is not null)
{
foreach (string relatedProperty in RelatedEntitiesCollections.ReferenceProperties)
await entity.Collection(relatedProperty).LoadAsync();
}
}
}
41 changes: 41 additions & 0 deletions src/DotNetElements.Web.MudBlazor/CrudOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
namespace DotNetElements.Web.MudBlazor;

public class CrudOptions<TModel>
{
public string BaseEndpointUri { get; private init; }

private string getAllEndpoint = null!;
public string GetAllEndpoint
{
get => getAllEndpoint;
init
{
getAllEndpoint = $"{BaseEndpointUri.TrimEnd('/')}";

if (!string.IsNullOrEmpty(value))
getAllEndpoint += $"/{value.TrimStart('/')}";
}
}

private string getAllWithDetailsEndpoint = null!;
public string GetAllWithDetailsEndpoint
{
get => getAllWithDetailsEndpoint;
init
{
getAllWithDetailsEndpoint = $"{BaseEndpointUri.TrimEnd('/')}";

if (!string.IsNullOrEmpty(value))
getAllWithDetailsEndpoint += $"/{value.TrimStart('/')}";
}
}

public string GetDetailsEndpoint(string id) => $"{BaseEndpointUri.TrimEnd('/')}/{id}/details";

public CrudOptions(string baseEndpointUri)
{
BaseEndpointUri = baseEndpointUri;
GetAllEndpoint = "";
GetAllWithDetailsEndpoint = "";
}
}
137 changes: 137 additions & 0 deletions src/DotNetElements.Web.MudBlazor/CrudService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
namespace DotNetElements.Web.MudBlazor;

public interface ICrudService<TKey, TModel, TDetails, TEditModel>
where TKey : notnull, IEquatable<TKey>
where TModel : IModel<TKey>
where TDetails : ModelDetails
where TEditModel : IMapFromModel<TEditModel, TModel>, ICreateNew<TEditModel>
{
Task<Result<TModel>> CreateEntryAsync(TEditModel editModel);
Task<Result> DeleteEntryAsync(TModel model);
Task<Result<List<TModel>>> GetAllEntriesAsync();
Task<Result<List<ModelWithDetails<TModel, TDetails>>>> GetAllEntriesWithDetailsAsync();
Task<Result> GetEntryDetailsAsync(ModelWithDetails<TModel, TDetails> modelWithDetails);
Task<Result<TModel>> UpdateEntryAsync(TEditModel editModel);
}

public class CrudService<TKey, TModel, TDetails, TEditModel> : ICrudService<TKey, TModel, TDetails, TEditModel> where TKey : notnull, IEquatable<TKey>
where TModel : IModel<TKey>
where TDetails : ModelDetails
where TEditModel : IMapFromModel<TEditModel, TModel>, ICreateNew<TEditModel>
{
protected readonly ISnackbar Snackbar;
protected readonly HttpClient HttpClient;
protected readonly CrudOptions<TModel> Options;

public CrudService(ISnackbar snackbar, HttpClient httpClient, CrudOptions<TModel> options)
{
Snackbar = snackbar;
HttpClient = httpClient;
Options = options;
}

public virtual async Task<Result<TModel>> CreateEntryAsync(TEditModel editModel)
{
Result<TModel> result = await HttpClient.PutAsJsonWithResultAsync<TEditModel, TModel>(Options.BaseEndpointUri, editModel);

// todo add logging
// todo wrap Snackbar call in bool option NotifyUser
// todo add function OnDeleteSuccess
if (result.IsOk)
{
Snackbar.Add("Entry saved", Severity.Success);
}
else
{
Snackbar.Add("Failed to save entry", Severity.Error);
}

return result;
}

public virtual async Task<Result<TModel>> UpdateEntryAsync(TEditModel editModel)
{
Result<TModel> result = await HttpClient.PostAsJsonWithResultAsync<TEditModel, TModel>(Options.BaseEndpointUri, editModel);

// todo add logging
// todo wrap Snackbar call in bool option NotifyUser
// todo add function OnDeleteSuccess
if (result.IsOk)
{
Snackbar.Add("Changes saved", Severity.Success);
}
else
{
Snackbar.Add("Failed to save changes", Severity.Error);
}

return result;
}

public virtual async Task<Result> DeleteEntryAsync(TModel model)
{
Result result = await HttpClient.DeleteWithResultAsync(Options.BaseEndpointUri, model);

// todo add logging
// todo wrap Snackbar call in bool option NotifyUser
// todo add function OnDeleteSuccess
if (result.IsOk)
{
Snackbar.Add("Entry deleted", Severity.Success);
}
else
{
Snackbar.Add("Failed to delete entry", Severity.Error);
}

return result;
}

public virtual async Task<Result> GetEntryDetailsAsync(ModelWithDetails<TModel, TDetails> modelWithDetails)
{
Result<TDetails> detailsResult = await HttpClient.GetFromJsonWithResultAsync<TDetails>(Options.GetDetailsEndpoint(modelWithDetails.Value.Id.ToString()));

Check warning on line 92 in src/DotNetElements.Web.MudBlazor/CrudService.cs

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu-latest

Possible null reference argument for parameter 'id' in 'string CrudOptions<TModel>.GetDetailsEndpoint(string id)'.

Check warning on line 92 in src/DotNetElements.Web.MudBlazor/CrudService.cs

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu-latest

Possible null reference argument for parameter 'id' in 'string CrudOptions<TModel>.GetDetailsEndpoint(string id)'.

// todo add logging
// todo wrap Snackbar call in bool option NotifyUser
// todo add function OnDeleteSuccess
if (detailsResult.IsFail)
{
Snackbar.Add($"Failed to fetch details.\n{detailsResult.ErrorMessage}", Severity.Error);
return detailsResult;
}

modelWithDetails.Details = detailsResult.Value;

return detailsResult;
}

public virtual async Task<Result<List<TModel>>> GetAllEntriesAsync()
{
Result<List<TModel>> result = await HttpClient.GetFromJsonWithResultAsync<List<TModel>>(Options.GetAllEndpoint);

// todo add logging
// todo wrap Snackbar call in bool option NotifyUser
// todo add function OnDeleteSuccess
if (result.IsFail)
{
Snackbar.Add("Failed to fetch entries from server", Severity.Error);
}

return result;
}

public virtual async Task<Result<List<ModelWithDetails<TModel, TDetails>>>> GetAllEntriesWithDetailsAsync()
{
Result<List<ModelWithDetails<TModel, TDetails>>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync<TModel, TDetails>(Options.GetAllEndpoint);

// todo add logging
// todo wrap Snackbar call in bool option NotifyUser
// todo add function OnDeleteSuccess
if (result.IsFail)
{
Snackbar.Add("Failed to fetch entries from server", Severity.Error);
}

return result;
}
}
5 changes: 3 additions & 2 deletions src/DotNetElements.Web.MudBlazor/CrudTable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public static class CrudTable
public static readonly DialogOptions DefaultEditDialogOptions = new DialogOptions() { MaxWidth = MaxWidth.Medium, FullWidth = true, DisableBackdropClick = true };
}

// todo used CrudService
public abstract class CrudTable<TKey, TModel, TDetails, TEditModel, TEditDialog> : MudComponentBase
where TKey : notnull, IEquatable<TKey>
where TModel : IModel<TKey>
Expand Down Expand Up @@ -131,7 +132,7 @@ protected async Task OnShowEntryDetails(ModelWithDetails<TModel, TDetails> conte
return;
}

Result<TDetails> details = await HttpClient.GetFromJsonWithResultAsync<TDetails>(string.Format(options.GetDetailsEndpoint(context.Value.Id.ToString()), context.Value.Id));
Result<TDetails> details = await HttpClient.GetFromJsonWithResultAsync<TDetails>(options.GetDetailsEndpoint(context.Value.Id.ToString()));

Check warning on line 135 in src/DotNetElements.Web.MudBlazor/CrudTable.cs

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu-latest

Possible null reference argument for parameter 'id' in 'string CrudOptions<TModel>.GetDetailsEndpoint(string id)'.

Check warning on line 135 in src/DotNetElements.Web.MudBlazor/CrudTable.cs

View workflow job for this annotation

GitHub Actions / build-and-test-ubuntu-latest

Possible null reference argument for parameter 'id' in 'string CrudOptions<TModel>.GetDetailsEndpoint(string id)'.

if (details.IsFail)
{
Expand All @@ -145,7 +146,7 @@ protected async Task OnShowEntryDetails(ModelWithDetails<TModel, TDetails> conte

protected async Task UpdateEntries()
{
Result<List<ModelWithDetails<TModel, TDetails>>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync<TModel, TDetails>(options.GetAllEndpoint);
Result<List<ModelWithDetails<TModel, TDetails>>> result = await HttpClient.GetModelWithDetailsListFromJsonAsync<TModel, TDetails>(options.GetAllWithDetailsEndpoint);

if (result.IsFail)
{
Expand Down
23 changes: 2 additions & 21 deletions src/DotNetElements.Web.MudBlazor/CrudTableOptions.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,14 @@
namespace DotNetElements.Web.MudBlazor;

public class CrudTableOptions<TModel>
public class CrudTableOptions<TModel> : CrudOptions<TModel>
{
public string BaseEndpointUri { get; private init; }

private string getAllEndpoint = null!;
public string GetAllEndpoint
{
get => getAllEndpoint;
init
{
getAllEndpoint = $"{BaseEndpointUri.TrimEnd('/')}";

if (!string.IsNullOrEmpty(value))
getAllEndpoint += $"/{value.TrimStart('/')}";
}
}

public required string DeleteEntryLabel { get; set; }
public required Func<TModel, string> DeleteEntryValue { get; set; }

public string GetDetailsEndpoint(string id) => $"{BaseEndpointUri.TrimEnd('/')}/{id}/details";

public DialogOptions EditDialogOptions { get; init; }

public CrudTableOptions(string baseEndpointUri)
public CrudTableOptions(string baseEndpointUri) : base(baseEndpointUri)
{
BaseEndpointUri = baseEndpointUri;
GetAllEndpoint = "";
EditDialogOptions = CrudTable.DefaultEditDialogOptions;
}

Expand Down
33 changes: 33 additions & 0 deletions src/DotNetElements.Web.MudBlazor/DneDataAnnotationsValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
namespace DotNetElements.Web.MudBlazor;

public class DneDataAnnotationsValidator : DataAnnotationsValidator
{
#if DEBUG
[CascadingParameter] EditContext? DebugEditContext { get; set; }

protected override void OnInitialized()
{
base.OnInitialized();

if (DebugEditContext is null)
{
throw new InvalidOperationException($"{nameof(DneDataAnnotationsValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(DneDataAnnotationsValidator)} " +
$"inside an EditForm.");
}

DebugEditContext.OnValidationRequested += DebugEditContext_OnValidationRequested;
}

protected override void Dispose(bool disposing)
{
if (disposing && DebugEditContext is not null)
DebugEditContext.OnValidationRequested -= DebugEditContext_OnValidationRequested;
}

private void DebugEditContext_OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
DebugEditContext.LogDebugInfo(); // Debug only
}
#endif
}
Loading

0 comments on commit 3d63a85

Please sign in to comment.