diff --git a/DotNetElements.sln b/DotNetElements.sln
index db93d90..2846965 100644
--- a/DotNetElements.sln
+++ b/DotNetElements.sln
@@ -5,7 +5,9 @@ VisualStudioVersion = 17.8.34330.188
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetElements.CrudExample", "samples\DotNetElements.CrudExample\DotNetElements.CrudExample.csproj", "{5841B4C6-1339-412F-97F4-D17C6E3D3D24}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetElements.Core", "src\DotNetElements.Core\DotNetElements.Core.csproj", "{1CEE4FCD-1A35-4365-A6A1-6F05937B4E3A}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetElements.Core", "src\DotNetElements.Core\DotNetElements.Core.csproj", "{1CEE4FCD-1A35-4365-A6A1-6F05937B4E3A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetElements.Core.Test", "test\DotNetElements.Core.Test\DotNetElements.Core.Test.csproj", "{9BA47821-EBE3-4290-877B-FE75340AE33E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -21,6 +23,10 @@ Global
{1CEE4FCD-1A35-4365-A6A1-6F05937B4E3A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1CEE4FCD-1A35-4365-A6A1-6F05937B4E3A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1CEE4FCD-1A35-4365-A6A1-6F05937B4E3A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9BA47821-EBE3-4290-877B-FE75340AE33E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9BA47821-EBE3-4290-877B-FE75340AE33E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9BA47821-EBE3-4290-877B-FE75340AE33E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9BA47821-EBE3-4290-877B-FE75340AE33E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d599b20
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Felix-CodingClimber
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bcb49ee
--- /dev/null
+++ b/README.md
@@ -0,0 +1,225 @@
+
+
+
+
+
+[![Uptime Robot status](https://img.shields.io/uptimerobot/status/m796178913-32633fee88c8a4bfdc895a64?label=DOCs%20STATUS)](https://dotnet-elements.felixstrauss.dev/)
+
+
+
+
+
+
+
+
+
DotNet Elements
+
+
+ Opinionated framework to build .NET applications fast and easy while focusing more on the final product and less on writing low-level code.
+
+ Explore the docs »
+
+
+
+ Report Bug
+ ·
+ Request Feature
+
+
+
+
+
+
+
+ Table of Contents
+
+ -
+ About The Project
+
+
+
+ - License
+
+
+
+
+
+
+
+## About The Project
+
+> [!CAUTION]
+> Framework is work in progress and not considered production ready (while still used in some personal projects). Feel free to try it out and share your thoughts.
+
+(back to top)
+
+
+
+### Built With
+
+[![NET][.NET]][.NET-url]
+
+(back to top)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## License
+
+Distributed under the MIT License. See `LICENSE.txt` for more information.
+
+(back to top)
+
+
+
+
+
+
+
+
+
+
+
+
+
+[.NET]: https://img.shields.io/badge/.NET-000000?style=for-the-badge&logo=dotnet&labelColor=512BD4
+[.NET-url]: https://dotnet.microsoft.com/en-us/
+
+
diff --git a/brand/Logo_Modified/Logo_Small.png b/brand/Logo_Modified/Logo_Small.png
new file mode 100644
index 0000000..848307a
Binary files /dev/null and b/brand/Logo_Modified/Logo_Small.png differ
diff --git a/samples/DotNetElements.CrudExample/Components/Pages/Crud.razor b/samples/DotNetElements.CrudExample/Components/Pages/Crud.razor
index 51e89c6..a08f037 100644
--- a/samples/DotNetElements.CrudExample/Components/Pages/Crud.razor
+++ b/samples/DotNetElements.CrudExample/Components/Pages/Crud.razor
@@ -208,11 +208,11 @@ else
if (result.Canceled)
return;
- Result createdTag = await tagRepository.CreateAsync(editTagModel.MapToEntity());
+ CrudResult createdTag = await tagRepository.CreateAsync(editTagModel.MapToEntity());
if (createdTag.IsFail)
{
- snackbar.Add("Failed to create tag", Severity.Error);
+ snackbar.Add($"Failed to create tag.\n{createdTag.Message}", Severity.Error);
return;
}
@@ -238,11 +238,11 @@ else
if (result.Canceled)
return;
- Result updatedTag = await tagRepository.UpdateAsync(editTagModel.Id, editTagModel);
+ CrudResult updatedTag = await tagRepository.UpdateAsync(editTagModel.Id, editTagModel);
if (updatedTag.IsFail)
{
- snackbar.Add("Failed to update tag", Severity.Error);
+ snackbar.Add($"Failed to update tag.\n{updatedTag.Message}", Severity.Error);
return;
}
@@ -259,11 +259,11 @@ else
if (confirmResult.IsFail)
return;
- Result deleteResult = await tagRepository.DeleteAsync(tag.Id);
+ CrudResult deleteResult = await tagRepository.DeleteAsync(tag);
if(deleteResult.IsFail)
{
- snackbar.Add("Failed to delete tag", Severity.Error);
+ snackbar.Add($"Failed to delete tag.\n{deleteResult.Message}", Severity.Error);
return;
}
@@ -281,11 +281,11 @@ else
return;
}
- Result details = await tagRepository.GetAuditedModelDetailsByIdAsync(context.Value.Id);
+ CrudResult details = await tagRepository.GetAuditedModelDetailsByIdAsync(context.Value.Id);
if(details.IsFail)
{
- snackbar.Add("Failed to fetch tag details", Severity.Error);
+ snackbar.Add($"Failed to fetch tag details.\n{details.Message}", Severity.Error);
return;
}
@@ -313,11 +313,11 @@ else
if (result.Canceled)
return;
- Result createdBlogPost = await blogPostRepository.CreateAsync(editBlogPostModel.MapToEntity());
+ CrudResult createdBlogPost = await blogPostRepository.CreateAsync(editBlogPostModel.MapToEntity());
if (createdBlogPost.IsFail)
{
- snackbar.Add("Failed to create blog post", Severity.Error);
+ snackbar.Add($"Failed to create blog post.\n{createdBlogPost.Message}", Severity.Error);
return;
}
@@ -346,11 +346,11 @@ else
if (result.Canceled)
return;
- Result updatedBlogPost = await blogPostRepository.UpdateAsync(editBlogPostModel.Id, editBlogPostModel);
+ CrudResult updatedBlogPost = await blogPostRepository.UpdateAsync(editBlogPostModel.Id, editBlogPostModel);
if (updatedBlogPost.IsFail)
{
- snackbar.Add("Failed to update blog post", Severity.Error);
+ snackbar.Add($"Failed to update blog post.\n{updatedBlogPost.Message}", Severity.Error);
return;
}
@@ -366,11 +366,11 @@ else
if (confirmResult.IsFail)
return;
- Result deleteResult = await blogPostRepository.DeleteAsync(blogPost.Id);
+ CrudResult deleteResult = await blogPostRepository.DeleteAsync(blogPost);
if(deleteResult.IsFail)
{
- snackbar.Add("Failed to delete blog post", Severity.Error);
+ snackbar.Add($"Failed to delete blog post.\n{deleteResult.Message}", Severity.Error);
return;
}
@@ -387,11 +387,11 @@ else
return;
}
- Result details = await blogPostRepository.GetAuditedModelDetailsByIdAsync(context.Value.Id);
+ CrudResult details = await blogPostRepository.GetAuditedModelDetailsByIdAsync(context.Value.Id);
if(details.IsFail)
{
- snackbar.Add("Failed to fetch blog post details", Severity.Error);
+ snackbar.Add($"Failed to fetch blog post details.\n{details.Message}", Severity.Error);
return;
}
diff --git a/samples/DotNetElements.CrudExample/Modules/BlogPostModule/BlogPostModule.cs b/samples/DotNetElements.CrudExample/Modules/BlogPostModule/BlogPostModule.cs
index 904cd2b..05615c1 100644
--- a/samples/DotNetElements.CrudExample/Modules/BlogPostModule/BlogPostModule.cs
+++ b/samples/DotNetElements.CrudExample/Modules/BlogPostModule/BlogPostModule.cs
@@ -1,4 +1,6 @@
-namespace DotNetElements.CrudExample.Modules.BlogPostModule;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DotNetElements.CrudExample.Modules.BlogPostModule;
public sealed class BlogPostModule : IModule
{
@@ -16,25 +18,25 @@ public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapPut(BaseUrl, async (EditBlogPostModel blogPost, BlogPostRepository blogPostRepo) =>
{
- Result result = await blogPostRepo.CreateAsync(blogPost.MapToEntity());
+ CrudResult result = await blogPostRepo.CreateAsync(blogPost.MapToEntity());
- return result.IsOk ? Results.Ok(result.Value.MapToModel()) : Results.Conflict(result.Error);
+ return result.MapToHttpResultWithProjection(entity => entity.MapToModel());
});
endpoints.MapPost(BaseUrl, async (EditBlogPostModel blogPost, BlogPostRepository blogPostRepo) =>
{
- Result result = await blogPostRepo.UpdateAsync(blogPost.Id, blogPost);
+ CrudResult result = await blogPostRepo.UpdateAsync(blogPost.Id, blogPost);
- return result.IsOk ? Results.Ok(result.Value.MapToModel()) : Results.NotFound(result.Error);
+ return result.MapToHttpResultWithProjection(entity => entity.MapToModel());
});
- endpoints.MapDelete($"{BaseUrl}/{{id}}", async (Guid id, BlogPostRepository blogPostRepo) =>
+ endpoints.MapDelete(BaseUrl, async ([FromBody] BlogPostModel blogPost, BlogPostRepository blogPostRepo) =>
{
- Result result = await blogPostRepo.DeleteAsync(id);
+ CrudResult result = await blogPostRepo.DeleteAsync(blogPost);
- return result.IsOk ? Results.Ok() : Results.NotFound(result.Error);
+ return result.MapToHttpResult();
});
@@ -46,9 +48,9 @@ public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints)
endpoints.MapGet($"{BaseUrl}/{{id}}", async (Guid id, BlogPostRepository blogPostRepo, CancellationToken cancellationToken) =>
{
- Result result = await blogPostRepo.GetByIdWithProjectionAsync(id, query => query.MapToModel(), cancellationToken: cancellationToken);
+ CrudResult result = await blogPostRepo.GetByIdWithProjectionAsync(id, query => query.MapToModel(), cancellationToken: cancellationToken);
- return result.IsOk ? Results.Ok(result.Value) : Results.NotFound(result.Error);
+ return result.MapToHttpResult();
});
return endpoints;
diff --git a/samples/DotNetElements.CrudExample/Modules/TagModule/TagModule.cs b/samples/DotNetElements.CrudExample/Modules/TagModule/TagModule.cs
index af44910..5370507 100644
--- a/samples/DotNetElements.CrudExample/Modules/TagModule/TagModule.cs
+++ b/samples/DotNetElements.CrudExample/Modules/TagModule/TagModule.cs
@@ -1,4 +1,6 @@
-namespace DotNetElements.CrudExample.Modules.TagModule;
+using Microsoft.AspNetCore.Mvc;
+
+namespace DotNetElements.CrudExample.Modules.TagModule;
public sealed class TagModule : IModule
{
@@ -16,25 +18,25 @@ public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints)
{
endpoints.MapPut(BaseUrl, async (EditTagModel tag, TagRepository tagRepo) =>
{
- Result result = await tagRepo.CreateAsync(tag.MapToEntity(), entity => entity.Label == tag.Label);
+ CrudResult result = await tagRepo.CreateAsync(tag.MapToEntity(), entity => entity.Label == tag.Label);
- return result.IsOk ? Results.Ok(result.Value.MapToModel()) : Results.Conflict(result.Error);
+ return result.MapToHttpResultWithProjection(entity => entity.MapToModel());
});
endpoints.MapPost(BaseUrl, async (EditTagModel tag, TagRepository tagRepo) =>
{
- Result result = await tagRepo.UpdateAsync(tag.Id, tag);
+ CrudResult result = await tagRepo.UpdateAsync(tag.Id, tag);
- return result.IsOk ? Results.Ok(result.Value.MapToModel()) : Results.NotFound(result.Error);
+ return result.MapToHttpResultWithProjection(entity => entity.MapToModel());
});
- endpoints.MapDelete($"{BaseUrl}/{{id}}", async (Guid id, TagRepository tagRepo) =>
+ endpoints.MapDelete(BaseUrl, async ([FromBody] TagModel tag, TagRepository tagRepo) =>
{
- Result result = await tagRepo.DeleteAsync(id);
+ CrudResult result = await tagRepo.DeleteAsync(tag);
- return result.IsOk ? Results.Ok() : Results.NotFound(result.Error);
+ return result.MapToHttpResult();
});
@@ -46,9 +48,9 @@ public IEndpointRouteBuilder MapEndpoints(IEndpointRouteBuilder endpoints)
endpoints.MapGet($"{BaseUrl}/{{id}}", async (Guid id, TagRepository tagRepo, CancellationToken cancellationToken) =>
{
- Result result = await tagRepo.GetByIdWithProjectionAsync(id, query => query.MapToModel(), cancellationToken: cancellationToken);
+ CrudResult result = await tagRepo.GetByIdWithProjectionAsync(id, query => query.MapToModel(), cancellationToken: cancellationToken);
- return result.IsOk ? Results.Ok(result.Value) : Results.NotFound(result.Error);
+ return result.MapToHttpResult();
});
return endpoints;
diff --git a/samples/DotNetElements.CrudExample/Program.cs b/samples/DotNetElements.CrudExample/Program.cs
index d6256bc..4d8e9ad 100644
--- a/samples/DotNetElements.CrudExample/Program.cs
+++ b/samples/DotNetElements.CrudExample/Program.cs
@@ -3,7 +3,7 @@
using DotNetElements.CrudExample.Components;
using DotNetElements.CrudExample.Modules.BlogPostModule;
-var builder = WebApplication.CreateBuilder(args);
+WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents()
@@ -21,7 +21,7 @@
builder.Services.RegisterModules(typeof(BlogPostModule).Assembly);
-var app = builder.Build();
+WebApplication app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
diff --git a/samples/DotNetElements.CrudExample/TestDb.db b/samples/DotNetElements.CrudExample/TestDb.db
index 5b3afd0..0b750a0 100644
Binary files a/samples/DotNetElements.CrudExample/TestDb.db and b/samples/DotNetElements.CrudExample/TestDb.db differ
diff --git a/samples/DotNetElements.CrudExample/TestDb.db-shm b/samples/DotNetElements.CrudExample/TestDb.db-shm
index 9e2a72b..93a2d2b 100644
Binary files a/samples/DotNetElements.CrudExample/TestDb.db-shm and b/samples/DotNetElements.CrudExample/TestDb.db-shm differ
diff --git a/samples/DotNetElements.CrudExample/TestDb.db-wal b/samples/DotNetElements.CrudExample/TestDb.db-wal
index aafff11..331d652 100644
Binary files a/samples/DotNetElements.CrudExample/TestDb.db-wal and b/samples/DotNetElements.CrudExample/TestDb.db-wal differ
diff --git a/src/DotNetElements.Core/Core/IHasVersion.cs b/src/DotNetElements.Core/Core/Contracts.cs
similarity index 85%
rename from src/DotNetElements.Core/Core/IHasVersion.cs
rename to src/DotNetElements.Core/Core/Contracts.cs
index c7ab3a3..e9cc854 100644
--- a/src/DotNetElements.Core/Core/IHasVersion.cs
+++ b/src/DotNetElements.Core/Core/Contracts.cs
@@ -1,5 +1,13 @@
namespace DotNetElements.Core;
+public interface IHasKey
+ where TKey : notnull
+{
+ TKey Id { get; }
+
+ bool HasKey => !Id.Equals(default(TKey));
+}
+
public interface IHasVersionReadOnly
{
Guid Version { get; }
diff --git a/src/DotNetElements.Core/Core/EntityBase.cs b/src/DotNetElements.Core/Core/EntityBase.cs
index 6be31c4..e747dc3 100644
--- a/src/DotNetElements.Core/Core/EntityBase.cs
+++ b/src/DotNetElements.Core/Core/EntityBase.cs
@@ -2,22 +2,11 @@
namespace DotNetElements.Core;
-public interface IEntity
-{
- const string NoKey = "NOKEY";
-
- abstract bool HasKey { get; }
+public interface IEntity : IHasKey
+ where TKey : notnull;
- string Key { get; }
-}
-
-public interface IEntity : IEntity
+public interface ICreationAuditedEntity : IEntity
where TKey : notnull
-{
- TKey Id { get; }
-}
-
-public interface ICreationAuditedEntity : IEntity
{
Guid CreatorId { get; }
@@ -26,7 +15,8 @@ public interface ICreationAuditedEntity : IEntity
void SetCreationAudited(Guid creatorId, DateTimeOffset creationTime);
}
-public interface IAuditedEntity : ICreationAuditedEntity
+public interface IAuditedEntity : ICreationAuditedEntity
+ where TKey : notnull
{
Guid? LastModifierId { get; }
@@ -76,13 +66,9 @@ public abstract class Entity : Entity, IEntity
where TKey : notnull
{
public TKey Id { get; protected set; } = default!;
-
- public bool HasKey => !Id.Equals(default(TKey));
-
- string IEntity.Key => HasKey ? Id.ToString()! : IEntity.NoKey;
}
-public class CreationAuditedEntity : Entity, ICreationAuditedEntity
+public class CreationAuditedEntity : Entity, ICreationAuditedEntity
where TKey : notnull
{
public Guid CreatorId { get; private set; }
@@ -99,7 +85,7 @@ public void SetCreationAudited(Guid creatorId, DateTimeOffset creationTime)
}
}
-public class AuditedEntity : CreationAuditedEntity, IAuditedEntity
+public class AuditedEntity : CreationAuditedEntity, IAuditedEntity
where TKey : notnull
{
public Guid? LastModifierId { get; private set; }
diff --git a/src/DotNetElements.Core/Core/IReadOnlyRepository.cs b/src/DotNetElements.Core/Core/IReadOnlyRepository.cs
index 3c78a71..a766f44 100644
--- a/src/DotNetElements.Core/Core/IReadOnlyRepository.cs
+++ b/src/DotNetElements.Core/Core/IReadOnlyRepository.cs
@@ -1,13 +1,14 @@
using System.Linq.Expressions;
namespace DotNetElements.Core;
+
public interface IReadOnlyRepository
where TEntity : Entity
where TKey : notnull
{
Task> GetAllAsync(CancellationToken cancellationToken = default);
- Task> GetAllAsync(
+ Task> GetAllFilteredAsync(
Expression>? filter = null,
Expression>? orderBy = null,
bool descending = true,
@@ -39,20 +40,20 @@ Task> GetAllWithProjectionAsync(
bool descending = true,
CancellationToken cancellationToken = default);
- Task> GetByIdAsync(TKey id, CancellationToken cancellationToken = default);
+ Task> GetByIdAsync(TKey id, CancellationToken cancellationToken = default);
- Task> GetByIdAsync(
+ Task> GetByIdFilteredAsync(
TKey id,
Expression>? filter = null,
CancellationToken cancellationToken = default);
- Task> GetByIdWithProjectionAsync(
+ Task> GetByIdWithProjectionAsync(
TKey id,
Expression, IQueryable>> selector,
Expression>? filter = null,
CancellationToken cancellationToken = default);
- Task> GetAuditedModelDetailsByIdAsync(
+ Task> GetAuditedModelDetailsByIdAsync(
TKey id,
CancellationToken cancellationToken = default)
where TAuditedEntity : AuditedEntity;
diff --git a/src/DotNetElements.Core/Core/IRepository.cs b/src/DotNetElements.Core/Core/IRepository.cs
index 3bd9980..7e4114b 100644
--- a/src/DotNetElements.Core/Core/IRepository.cs
+++ b/src/DotNetElements.Core/Core/IRepository.cs
@@ -1,24 +1,22 @@
using System.Linq.Expressions;
namespace DotNetElements.Core;
+
public interface IRepository : IReadOnlyRepository
where TEntity : Entity
where TKey : notnull
{
Task ClearTable();
- Task> CreateAsync(TEntity entity, Expression>? checkDuplicate = null);
+ Task> CreateAsync(TEntity entity, Expression>? checkDuplicate = null);
- Task> CreateOrUpdateAsync(
- TKey id,
- TSelf entity,
- Expression>? checkDuplicate = null)
+ Task> CreateOrUpdateAsync(TKey id, TSelf entity, Expression>? checkDuplicate = null)
where TSelf : Entity, IUpdatable;
- Task DeleteAsync(TKey id);
+ Task DeleteAsync(TEntityToDelete entityToDelete)
+ where TEntityToDelete : IHasKey, IHasVersionReadOnly;
- Task> UpdateAsync(
- TKey id,
- TFrom from)
- where TUpdatableEntity : Entity, IUpdatable;
+ Task> UpdateAsync(TKey id, TFrom from)
+ where TUpdatableEntity : Entity, IUpdatable
+ where TFrom : notnull;
}
\ No newline at end of file
diff --git a/src/DotNetElements.Core/Core/ManagedRepository.cs b/src/DotNetElements.Core/Core/ManagedRepository.cs
index 63716f7..46f7d8e 100644
--- a/src/DotNetElements.Core/Core/ManagedRepository.cs
+++ b/src/DotNetElements.Core/Core/ManagedRepository.cs
@@ -14,14 +14,14 @@ public ManagedRepository(IScopedRepositoryFactory re
this.repositoryFactory = repositoryFactory;
}
- public Task> CreateAsync(TEntity entity, Expression>? checkDuplicate = null)
+ public Task> CreateAsync(TEntity entity, Expression>? checkDuplicate = null)
{
using var repository = repositoryFactory.Create();
return repository.Inner.CreateAsync(entity, checkDuplicate);
}
- public Task> CreateOrUpdateAsync(TKey id, TSelf entity, Expression>? checkDuplicate = null)
+ public Task> CreateOrUpdateAsync(TKey id, TSelf entity, Expression>? checkDuplicate = null)
where TSelf : Entity, IUpdatable
{
using var repository = repositoryFactory.Create();
@@ -29,11 +29,12 @@ public Task> CreateOrUpdateAsync(TKey id, TSelf entity, Exp
return repository.Inner.CreateOrUpdateAsync(id, entity, checkDuplicate);
}
- public Task DeleteAsync(TKey id)
+ public Task DeleteAsync(TEntityToDelete entityToDelete)
+ where TEntityToDelete : IHasKey, IHasVersionReadOnly
{
using var repository = repositoryFactory.Create();
- return repository.Inner.DeleteAsync(id);
+ return repository.Inner.DeleteAsync(entityToDelete);
}
public Task> GetAllAsync(CancellationToken cancellationToken = default)
@@ -43,7 +44,7 @@ public Task> GetAllAsync(CancellationToken cancellationTo
return repository.Inner.GetAllAsync(cancellationToken);
}
- public Task> GetAllAsync(
+ public Task> GetAllFilteredAsync(
Expression>? filter = null,
Expression>? orderBy = null,
bool descending = true,
@@ -51,7 +52,7 @@ public Task> GetAllAsync(
{
using var repository = repositoryFactory.Create();
- return repository.Inner.GetAllAsync(filter, orderBy, descending, cancellationToken);
+ return repository.Inner.GetAllFilteredAsync(filter, orderBy, descending, cancellationToken);
}
public Task> GetAllPagedAsync(
@@ -94,21 +95,21 @@ public Task> GetAllWithProjectionAsync(
return repository.Inner.GetAllWithProjectionAsync(selector, filter, orderBy, descending, cancellationToken);
}
- public Task> GetByIdAsync(TKey id, CancellationToken cancellationToken = default)
+ public Task> GetByIdAsync(TKey id, CancellationToken cancellationToken = default)
{
using var repository = repositoryFactory.Create();
return repository.Inner.GetByIdAsync(id, cancellationToken);
}
- public Task> GetByIdAsync(TKey id, Expression>? filter = null, CancellationToken cancellationToken = default)
+ public Task> GetByIdFilteredAsync(TKey id, Expression>? filter = null, CancellationToken cancellationToken = default)
{
using var repository = repositoryFactory.Create();
- return repository.Inner.GetByIdAsync(id, filter, cancellationToken);
+ return repository.Inner.GetByIdFilteredAsync(id, filter, cancellationToken);
}
- public Task> GetByIdWithProjectionAsync(
+ public Task> GetByIdWithProjectionAsync(
TKey id,
Expression, IQueryable>> selector,
Expression>? filter = null,
@@ -119,8 +120,9 @@ public Task> GetByIdWithProjectionAsync(
return repository.Inner.GetByIdWithProjectionAsync(id, selector, filter, cancellationToken);
}
- public Task> UpdateAsync(TKey id, TFrom from)
+ public Task> UpdateAsync(TKey id, TFrom from)
where TUpdatableEntity : Entity, IUpdatable
+ where TFrom : notnull
{
using var repository = repositoryFactory.Create();
@@ -134,7 +136,7 @@ public Task ClearTable()
return repository.Inner.ClearTable();
}
- public Task> GetAuditedModelDetailsByIdAsync(TKey id, CancellationToken cancellationToken = default)
+ public Task> GetAuditedModelDetailsByIdAsync(TKey id, CancellationToken cancellationToken = default)
where TAuditedEntity : AuditedEntity
{
using var repository = repositoryFactory.Create();
diff --git a/src/DotNetElements.Core/Core/ModelBase.cs b/src/DotNetElements.Core/Core/ModelBase.cs
index 6f32a3d..77d01a0 100644
--- a/src/DotNetElements.Core/Core/ModelBase.cs
+++ b/src/DotNetElements.Core/Core/ModelBase.cs
@@ -1,10 +1,7 @@
namespace DotNetElements.Core;
-public interface IModel
- where TKey : notnull
-{
- TKey Id { get; }
-}
+public interface IModel : IHasKey
+ where TKey : notnull;
public abstract class Model : IModel
where TKey : notnull
@@ -44,9 +41,8 @@ protected VersionedEditModel(Guid version) : base(default!, version) { }
protected VersionedEditModel(TKey id, Guid version) : base(id, version) { }
}
-public abstract class ModelDetails
-{
-}
+// todo interface?
+public abstract class ModelDetails;
public class CreationAuditedModelDetails : ModelDetails
{
diff --git a/src/DotNetElements.Core/Core/ReadOnlyRepository.cs b/src/DotNetElements.Core/Core/ReadOnlyRepository.cs
index 09756fc..4c7b02d 100644
--- a/src/DotNetElements.Core/Core/ReadOnlyRepository.cs
+++ b/src/DotNetElements.Core/Core/ReadOnlyRepository.cs
@@ -25,17 +25,14 @@ public ReadOnlyRepository(TDbContext dbContext)
Entities = dbContext.Set();
}
- public virtual async Task> GetByIdAsync(TKey id, CancellationToken cancellationToken = default)
+ public virtual async Task> GetByIdAsync(TKey id, CancellationToken cancellationToken = default)
{
TEntity? entity = await Entities.AsNoTracking().FirstOrDefaultAsync(WithId(id), cancellationToken);
- if (entity is null)
- return Result.EntityNotFound(id);
-
- return Result.Ok(entity);
+ return CrudResult.OkIfNotNull(entity, CrudError.NotFound, id.ToString());
}
- public async Task> GetByIdAsync(
+ public async Task> GetByIdFilteredAsync(
TKey id,
Expression>? filter = null,
CancellationToken cancellationToken = default)
@@ -47,13 +44,10 @@ public async Task> GetByIdAsync(
TEntity? entity = await entityQuery.FirstOrDefaultAsync(WithId(id), cancellationToken);
- if (entity is null)
- return Result.EntityNotFound(id);
-
- return Result.Ok(entity);
+ return CrudResult.OkIfNotNull(entity, CrudError.NotFound, id.ToString());
}
- public async Task> GetByIdWithProjectionAsync(
+ public async Task> GetByIdWithProjectionAsync(
TKey id,
Expression, IQueryable>> selector,
Expression>? filter = null,
@@ -68,10 +62,7 @@ public async Task> GetByIdWithProjectionAsync(
TProjection? projectedEntity = await selector.Compile().Invoke(entityQuery.Where(WithId(id))).FirstOrDefaultAsync(cancellationToken);
- if (projectedEntity is null)
- return Result.EntityNotFound(id);
-
- return Result.Ok(projectedEntity);
+ return CrudResult.OkIfNotNull(projectedEntity, CrudError.NotFound, id.ToString());
}
public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default)
@@ -79,7 +70,7 @@ public virtual async Task> GetAllAsync(CancellationToken
return await Entities.AsNoTracking().ToListAsync(cancellationToken);
}
- public async Task> GetAllAsync(
+ public async Task> GetAllFilteredAsync(
Expression>? filter = null,
Expression>? orderBy = null,
bool descending = true,
@@ -149,7 +140,7 @@ public async Task> GetAllPagedWithProjectionAsync> GetAuditedModelDetailsByIdAsync(TKey id, CancellationToken cancellationToken = default)
+ public async Task> GetAuditedModelDetailsByIdAsync(TKey id, CancellationToken cancellationToken = default)
where TAuditedEntity : AuditedEntity
{
DbSet localDbSet = DbContext.Set();
@@ -169,6 +160,6 @@ public async Task> GetAuditedModelDetailsByIdAsync> CreateAsync(TEntity entity, Expression>? checkDuplicate = null)
+ public virtual async Task> CreateAsync(TEntity entity, Expression>? checkDuplicate = null)
{
ArgumentNullException.ThrowIfNull(entity);
@@ -26,11 +26,11 @@ public virtual async Task> CreateAsync(TEntity entity, Expressio
bool isDuplicate = await Entities.AnyAsync(checkDuplicate);
if (isDuplicate)
- return Result.DuplicateEntity();
+ return CrudResult.DuplicateEntry();
}
// Set audit properties if needed
- if (entity is ICreationAuditedEntity auditedEntity)
+ if (entity is ICreationAuditedEntity auditedEntity)
auditedEntity.SetCreationAudited(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());
var createdEntity = Entities.Attach(entity);
@@ -40,7 +40,7 @@ public virtual async Task> CreateAsync(TEntity entity, Expressio
return createdEntity.Entity;
}
- public virtual async Task> CreateOrUpdateAsync(TKey id, TSelf entity, Expression>? checkDuplicate = null)
+ public virtual async Task> CreateOrUpdateAsync(TKey id, TSelf entity, Expression>? checkDuplicate = null)
where TSelf : Entity, IUpdatable
{
ArgumentNullException.ThrowIfNull(entity);
@@ -53,11 +53,11 @@ public virtual async Task> CreateOrUpdateAsync(TKey id, TSe
bool isDuplicate = await DbContext.Set().AnyAsync(checkDuplicate);
if (isDuplicate)
- return Result.DuplicateEntity();
+ return CrudResult.DuplicateEntry();
}
// Set audit properties if needed
- if (entity is ICreationAuditedEntity auditedEntity)
+ if (entity is ICreationAuditedEntity auditedEntity)
auditedEntity.SetCreationAudited(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());
var createdEntity = DbContext.Set().Attach(entity);
@@ -72,28 +72,29 @@ public virtual async Task> CreateOrUpdateAsync(TKey id, TSe
TSelf? existingEntity = await DbContext.Set().FirstOrDefaultAsync(WithId(id));
if (existingEntity is null)
- return Result.EntityNotFound(id);
+ return CrudResult.NotFound(id);
entity.Update(entity, this);
// Check if entity has changed and set audit properties if needed
if (DbContext.ChangeTracker.HasChanges())
{
- if (existingEntity is IAuditedEntity auditedEntity)
+ if (existingEntity is IAuditedEntity auditedEntity)
auditedEntity.SetModificationAudited(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());
- if (existingEntity is IHasVersion entityWithVersion)
- entityWithVersion.Version = Guid.NewGuid();
+ UpdateEntityVersion(existingEntity, entity);
- await DbContext.SaveChangesAsync();
+ if (!await SaveChangesWithVersionCheckAsync())
+ return CrudResult.ConcurrencyConflict();
}
return existingEntity;
}
}
- public virtual async Task> UpdateAsync(TKey id, TFrom from)
+ public virtual async Task> UpdateAsync(TKey id, TFrom from)
where TUpdatableEntity : Entity, IUpdatable
+ where TFrom : notnull
{
IQueryable query = DbContext.Set();
@@ -108,80 +109,62 @@ public virtual async Task> UpdateAsync(id));
if (existingEntity is null)
- return Result.EntityNotFound(id);
-
- // todo version 1 not ideal but working. Improve IReadVersion
- //if(from is IReadVersion readVersion && existingEntity is IHasVersion hasVersion)
- //{
- // if (readVersion.Version != hasVersion.Version)
- // throw new DbUpdateConcurrencyException();
- //}
+ return CrudResult.NotFound(id);
existingEntity.Update(from, this);
// Check if entity has changed and set audit properties if needed
if (DbContext.ChangeTracker.HasChanges())
{
- if (existingEntity is IAuditedEntity auditedEntity)
+ if (existingEntity is IAuditedEntity auditedEntity)
auditedEntity.SetModificationAudited(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());
- // todo needed for version 1
- //if (existingEntity is IHasVersion entityWithVersion)
- // entityWithVersion.Version = Guid.NewGuid();
-
- // todo version 2 not ideal but working. Improve IReadVersion
- if (existingEntity is IHasVersion entityWithVersion)
- {
- entityWithVersion.Version = Guid.NewGuid();
-
- // Set queried entities version to the version from the updating entity to detect weather or not the data has changed
- // between getting the data in the first place and updating it now
- if (from is IHasVersionReadOnly entityWithVersionReadOnly)
- DbContext.Entry(entityWithVersion).OriginalValues[nameof(IHasVersion.Version)] = entityWithVersionReadOnly.Version;
- }
+ UpdateEntityVersion(existingEntity, from);
- await DbContext.SaveChangesAsync();
+ if (!await SaveChangesWithVersionCheckAsync())
+ return CrudResult.ConcurrencyConflict();
}
return existingEntity;
}
- public virtual async Task DeleteAsync(TKey id)
+ // todo check if id and originalVersion is the right fit or if it would be better to get a entity as param
+ // Or consider to remove the default null value to force the user to be explicit
+ public virtual async Task DeleteAsync(TEntityToDelete entityToDelete)
+ where TEntityToDelete : IHasKey, IHasVersionReadOnly
{
- TEntity? entityToDelete = await Entities.FirstOrDefaultAsync(WithId(id));
+ TEntity? existingEntity = await Entities.FirstOrDefaultAsync(WithId(entityToDelete.Id));
- if (entityToDelete is null)
- return Result.EntityNotFound(id);
+ if (existingEntity is null)
+ return CrudResult.NotFound(entityToDelete.Id);
- if (entityToDelete is IDeletionAuditedEntity deletionAuditedEntity)
+ if (existingEntity is IDeletionAuditedEntity deletionAuditedEntity)
{
deletionAuditedEntity.Delete(CurrentUserProvider.GetCurrentUserId(), TimeProvider.GetUtcNow());
- if (entityToDelete is IHasVersion entityWithVersion)
- entityWithVersion.Version = Guid.NewGuid();
+ UpdateEntityVersion(existingEntity, entityToDelete.Version);
}
- else if (entityToDelete is IHasDeletionTime entityWithDeletionTime)
+ else if (existingEntity is IHasDeletionTime entityWithDeletionTime)
{
entityWithDeletionTime.Delete(TimeProvider.GetUtcNow());
- if (entityToDelete is IHasVersion entityWithVersion)
- entityWithVersion.Version = Guid.NewGuid();
+ UpdateEntityVersion(existingEntity, entityToDelete.Version);
}
- else if (entityToDelete is ISoftDelete softDeletableEntity)
+ else if (existingEntity is ISoftDelete softDeletableEntity)
{
softDeletableEntity.Delete();
- if (entityToDelete is IHasVersion entityWithVersion)
- entityWithVersion.Version = Guid.NewGuid();
+ UpdateEntityVersion(existingEntity, entityToDelete.Version);
}
else
{
- Entities.Remove(entityToDelete);
+ Entities.Remove(existingEntity);
}
- await DbContext.SaveChangesAsync();
+ if (!await SaveChangesWithVersionCheckAsync())
+ return CrudResult.ConcurrencyConflict();
- return Result.Ok();
+ return CrudResult.Ok();
}
public virtual async Task ClearTable()
@@ -189,6 +172,7 @@ public virtual async Task ClearTable()
await Entities.ExecuteDeleteAsync();
}
+ // todo protected would be better!
public TRelatedEntity AttachById(TRelatedEntityKey id)
where TRelatedEntity : Entity, IRelatedEntity
where TRelatedEntityKey : notnull
@@ -196,21 +180,68 @@ public TRelatedEntity AttachById(TRelatedEnti
return DbContext.Set().Attach(TRelatedEntity.CreateRefById(id)).Entity;
}
- protected async Task HardDeleteAsync(TKey id, Expression>? canBeDeleted = null)
+ protected void UpdateEntityVersion(TTargetEntity entityFromDb, Guid? originalVersion)
+ where TTargetEntity : notnull
+ {
+ if (entityFromDb is IHasVersion entityWithVersion)
+ {
+ entityWithVersion.Version = Guid.NewGuid();
+
+ SetOriginalVersionQueried(entityFromDb, originalVersion);
+ }
+ }
+
+ protected void UpdateEntityVersion(TTargetEntity entityFromDb, TSourceEntity updatedEntity)
+ where TTargetEntity : notnull
+ where TSourceEntity : notnull
+ {
+ if (entityFromDb is IHasVersion entityWithVersion)
+ {
+ entityWithVersion.Version = Guid.NewGuid();
+
+ if (updatedEntity is IHasVersionReadOnly entityWithVersionReadOnly)
+ SetOriginalVersionQueried(entityFromDb, entityWithVersionReadOnly.Version);
+ }
+ }
+
+ // Set queried entities version to the version of the updating entity to detect weather or not the data has changed
+ // between getting the data in the first place and updating it now
+ protected void SetOriginalVersionQueried(TTargetEntity entityFromDb, Guid? originalVersion)
+ where TTargetEntity : notnull
+ {
+ if (originalVersion is not null)
+ DbContext.Entry(entityFromDb).OriginalValues[nameof(IHasVersion.Version)] = originalVersion;
+ }
+
+ protected async Task HardDeleteAsync(TKey id, Expression