From 2a49f9f5fd24c55f571904455c3ba3b86582e3de Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber Date: Mon, 15 Jan 2024 15:10:21 +0100 Subject: [PATCH 1/6] Started implementing tests. --- DotNetElements.sln | 8 +- brand/Logo_Modified/Logo_Small.png | Bin 0 -> 17452 bytes .../Components/Pages/Crud.razor | 32 ++-- .../Modules/BlogPostModule/BlogPostModule.cs | 22 +-- .../Modules/TagModule/TagModule.cs | 22 +-- samples/DotNetElements.CrudExample/Program.cs | 4 +- samples/DotNetElements.CrudExample/TestDb.db | Bin 40960 -> 40960 bytes .../DotNetElements.CrudExample/TestDb.db-shm | Bin 32768 -> 32768 bytes .../DotNetElements.CrudExample/TestDb.db-wal | Bin 12392 -> 98912 bytes .../Core/{IHasVersion.cs => Contracts.cs} | 8 + src/DotNetElements.Core/Core/EntityBase.cs | 28 +--- .../Core/IReadOnlyRepository.cs | 11 +- src/DotNetElements.Core/Core/IRepository.cs | 18 +-- .../Core/ManagedRepository.cs | 26 ++-- src/DotNetElements.Core/Core/ModelBase.cs | 12 +- .../Core/ReadOnlyRepository.cs | 27 ++-- src/DotNetElements.Core/Core/Repository.cs | 143 +++++++++++------- .../Core/Result/CrudResult.cs | 128 ++++++++++++++++ .../Core/Result/CrudResultExtensions.cs | 44 ++++++ .../Core/Result/CrudResultHelper.cs | 64 ++++++++ src/DotNetElements.Core/Core/Result/Result.cs | 4 +- .../Core/Result/ResultHelper.cs | 2 +- .../DotNetElements.Core.Test.csproj | 33 ++++ test/DotNetElements.Core.Test/GlobalUsings.cs | 9 ++ .../ReadOnlyRespositoryTest.cs | 34 +++++ .../TestData/BlogPostModule/BlogPost.cs | 43 ++++++ .../TestData/BlogPostModule/BlogPostModel.cs | 32 ++++ .../BlogPostModule/BlogPostRepository.cs | 9 ++ .../ManagedBlogPostRepository.cs | 9 ++ .../BlogPostModule/MapperExtensions.cs | 48 ++++++ .../TestData/FakeEntities.cs | 12 ++ .../TestData/TagModule/FakeTagRepository.cs | 14 ++ .../TagModule/ManagedTagRepository.cs | 9 ++ .../TestData/TagModule/MapperExtensions.cs | 44 ++++++ .../TestData/TagModule/Tag.cs | 48 ++++++ .../TestData/TagModule/TagModel.cs | 27 ++++ .../TestData/TestDbContext.cs | 13 ++ .../Utils/FakeCurrentUserProvider.cs | 14 ++ .../Utils/FakeDbContextFactory.cs | 42 +++++ .../Utils/FakeRepositoryFactory.cs | 20 +++ .../Utils/RakeRepository.cs | 28 ++++ 41 files changed, 919 insertions(+), 172 deletions(-) create mode 100644 brand/Logo_Modified/Logo_Small.png rename src/DotNetElements.Core/Core/{IHasVersion.cs => Contracts.cs} (85%) create mode 100644 src/DotNetElements.Core/Core/Result/CrudResult.cs create mode 100644 src/DotNetElements.Core/Core/Result/CrudResultExtensions.cs create mode 100644 src/DotNetElements.Core/Core/Result/CrudResultHelper.cs create mode 100644 test/DotNetElements.Core.Test/DotNetElements.Core.Test.csproj create mode 100644 test/DotNetElements.Core.Test/GlobalUsings.cs create mode 100644 test/DotNetElements.Core.Test/ReadOnlyRespositoryTest.cs create mode 100644 test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPost.cs create mode 100644 test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostModel.cs create mode 100644 test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostRepository.cs create mode 100644 test/DotNetElements.Core.Test/TestData/BlogPostModule/ManagedBlogPostRepository.cs create mode 100644 test/DotNetElements.Core.Test/TestData/BlogPostModule/MapperExtensions.cs create mode 100644 test/DotNetElements.Core.Test/TestData/FakeEntities.cs create mode 100644 test/DotNetElements.Core.Test/TestData/TagModule/FakeTagRepository.cs create mode 100644 test/DotNetElements.Core.Test/TestData/TagModule/ManagedTagRepository.cs create mode 100644 test/DotNetElements.Core.Test/TestData/TagModule/MapperExtensions.cs create mode 100644 test/DotNetElements.Core.Test/TestData/TagModule/Tag.cs create mode 100644 test/DotNetElements.Core.Test/TestData/TagModule/TagModel.cs create mode 100644 test/DotNetElements.Core.Test/TestData/TestDbContext.cs create mode 100644 test/DotNetElements.Core.Test/Utils/FakeCurrentUserProvider.cs create mode 100644 test/DotNetElements.Core.Test/Utils/FakeDbContextFactory.cs create mode 100644 test/DotNetElements.Core.Test/Utils/FakeRepositoryFactory.cs create mode 100644 test/DotNetElements.Core.Test/Utils/RakeRepository.cs 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/brand/Logo_Modified/Logo_Small.png b/brand/Logo_Modified/Logo_Small.png new file mode 100644 index 0000000000000000000000000000000000000000..848307ae9c8e10dd08c626f14caafbb54e9294e1 GIT binary patch literal 17452 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 z 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 literal 0 HcmV?d00001 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 5b3afd08487a77ab8e0eee27cb80508a5cf93e07..0b750a03aec6c2ada7d03861e7fc65023fd392c7 100644 GIT binary patch delta 209 zcmZoTz|?SnX+w~`ild8(k&}swqpqQelZmd0nUR^Ug{y_Ju92C!fwMV~G&D1tygS}v z@;ZA59&;-L13fcK6H6mwlg+>E*#)@y;}{tE_Av16;oGxWP+$Y!2LAQ@H-HKk^G{x%ugL;br~y==!NwrKz`(y5D6xj0 d$v1SeoW08AJN0P7oT$P;V-(KwGeb;J0RR?{I)MNH delta 204 zcmZoTz|?SnX+w~`in)`Up_`Mjo362;fs?L@iHoDIg`p#e<>qQ?>g;A@X6iP1cf7^q zb@mQCMplN#dPZi(re?;Ln}6A}3ve?qFfj4$Vc?JB+p}3vU<2ReJ#mY;8|4`}LKQ

;4efqy;!4WPos{FB$`YqF>_hjIc{Xs|KzZ)V`%43t>O&+5&{ e92z=V&R%8moqALmAC^#{dZ^517Ka=BAmahpBRc#5 diff --git a/samples/DotNetElements.CrudExample/TestDb.db-shm b/samples/DotNetElements.CrudExample/TestDb.db-shm index 9e2a72b55cbae60a81338fdf466de9141ef90d78..93a2d2b5803c93aa815b15d939e83e52be912e62 100644 GIT binary patch delta 317 zcmZo@U}|V!s+V}A%K!q*K+MR%ARqyxxq$dd*vH2&8rFuzX|r->KV5ChoV0fB>eag| zNmUOt3JgHz{zn2(;WP$5D2ok9vjZ^;5VHa?j1Qw>;v7IZP9O$35+n!1AicA~_Zj=$JDVYYXQX-83*o`R{LIjQP_ nMu7py-2X@bDlEjn1ZA-RX)xaSk==U|V*)E9P>OluM`jZMeWEMA diff --git a/samples/DotNetElements.CrudExample/TestDb.db-wal b/samples/DotNetElements.CrudExample/TestDb.db-wal index aafff1194d1d8041b311c77044d96781fca3db26..331d652d35b07ec50186b5a28955f24687d2dd1b 100644 GIT binary patch literal 98912 zcmeI*Z-^Xc9l-I~yZq^0x!aIXX{j|AOYA{-mU-rR=9zg;`eJ5whC)+v)^5p*dbNdk zV1rQ-5R4&RsnQpD<0~cgg{e@~C?tcUe*B zA_{FLd@GI6MoM8*V}$Zes8tfmG)My9ZO4~F`ZIx?^Zhw#8Y-~94dZ?8neRKXXV+#} zB!Owlv=CVy#6l<9h$Kj35k@l2bFHii{N4;@cNW)7ZuMz**`PC&zR^+i;S9ICSx$E2 z1-H(B>++*doxW?bJlHNSP+zLAls_^MKmY**5I_I{1Q0*~0R#|0U>F2GQzZs6=4Luu zB^2S_x24>xZd_pJYk#=?(=Qx;uA47V-&HPUAbP7za@R7xt`6DhNK_*Qdq`H|1sZ;F| z4FV(LJXUTMk)K8GQ5rMorG6bKscg_mMi^ivk;`Fd7^`idbT6Y}t2?s2e1S*4{OX%` z|LWa;_VNYD7Py&CeD)Oq1Q0*~0R#|0009ILKw$F-%vOnNxZUlmFVL7gbNrR>*7lb9 z0=4>rS6`sMz~(t6*b@X0KmY**5I_I{1Q0-Avk44)eS!Aw+AVAK1rqIQ5+tqyf-hxL zXk}uN_;D^0E3?4LxKPdXI@TAEGt$nfaL%`l&}}=^Q7>PhufD*c&;DAza@UFdWxhbo zl@_S4P+wrP{nfBP2q1s}0tg_000IagfB*ucEKse!z&+!wi7KH8C()NLaL46S@4WWy zmwr*^3yir`0rCY#`CMV{2q1s}0tg_000IagfWYP!Xzw2DUX5?sDEG6>iXxYRyBc34 zBF#f7LTj`PLaCdX?p=-F_&O@A>qoGBLB9Lj-@LipsV{KQ zs~;R08zzbX0tg_000IagfB*srAb`LI3H<;42-bwRZR{2wIRyy*F1EWt*alwRT&L&7#kJIUVVYGA3?8A;o^h; z&bIBk5Pw)-V1r0m4FU)tfB*srAbSK>~@&(R$^>a6D%4`S$1Q0*~0R#|0009ILKmY**K3d@Z+IXu~#lS>I>f3)EnN^8?IR)0|5jOKmY**5I_I{1Q0-Aqy*ZBT+O??UCq1J zfhw1bd;R;Q42!JrlPncB%`>6Hyb!5TxrmEA%%W5#GWG{8cNZ!XhdSz%yBlaFDrgwr zNaO3yN`}8rp*wA!ZS7gDKDfRk(XB0bP07L4r`=@(8Tz)-cOcvh`{DvGzW(&@zkh4? zlTKXVyf<~8xWGvL@iSQj5I_I{1Q0*~0R#|0!1LN)aAASHgMqA`nl{S)EVH7>WgxUFlK+l@jJe6qR=IV-y|1rN z;rD*{{QJl6dvB@iQ|RsR=u=oaFc3fh0R#|0009ILKmY**5Ev?f2d-P6!avSmdhYX= z#gEH=1jGe~>Yy_j1Q0*~0R#|0009ILKmY**)&w4yFvcd$4bO`;xY$R zDvM?$Wv0`L`2u}?3P1PuFF*eEQ~rCsxWEeN2q1s}0tg_000IagfB*srATWXgRqrF% zQtn%~zQ9Z0d}QbR712q1s}0tg_000IagfB*sr+)M)PFS>pXVzr+` zm84Ral{mI1x!2h9YmqVPTBPqbMG6$ti)2ocwS6|@DneD$@+%Y}Ti3^;gzQE0N=CiK|Ab&ushbPLN?&b?T|MaV;Ke%xEtL1$JwK{Qu(uRQm0tg_000Iag zfB*srAb`LS3GCm}YE`N)P%$phS6|?*_aFZA!#{oUFP*r+is#xDG7vxj0R#|0009IL VKmY**5I|tW1nwDcO;m{s{0CzCeK`OC delta 303 zcmaFR!uBG;$-JJei9z>~1OtNr0|*>{v3J63?-bM2C9iJj1$quwK2WX`s$VxRsLn8}UOGifwCtY)AOIKYJQx_LqCl>>AkP}>-44mBz sT+B^jc50benVRdF8=ILL8Jc6abNYtqB`@TI)3-}FG0O2zZqt7R08RW-a{vGU 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>? canBeDeleted = null) where TSoftDeleteEntity : Entity, ISoftDelete { TSoftDeleteEntity? entityToDelete = await DbContext.Set().FirstOrDefaultAsync(WithId(id)); if (entityToDelete is null) - return Result.EntityNotFound(id); + return CrudResult.NotFound(id); if (canBeDeleted is not null && !canBeDeleted.Compile().Invoke(entityToDelete)) - return Result.Fail("Entity can not be deleted. It is still in use."); + return CrudResult.Fail("Entity can not be deleted. It is still in use."); DbContext.Set().Remove(entityToDelete); await DbContext.SaveChangesAsync(); - return Result.Ok(); + return CrudResult.Ok(); + } + + protected async Task SaveChangesWithVersionCheckAsync() + { + try + { + await DbContext.SaveChangesAsync(); + } + catch (DbUpdateConcurrencyException) + { + return false; + } + + return true; } } \ No newline at end of file diff --git a/src/DotNetElements.Core/Core/Result/CrudResult.cs b/src/DotNetElements.Core/Core/Result/CrudResult.cs new file mode 100644 index 0000000..7c7735e --- /dev/null +++ b/src/DotNetElements.Core/Core/Result/CrudResult.cs @@ -0,0 +1,128 @@ +namespace DotNetElements.Core; + +public enum CrudError +{ + Unknown, + NotFound, + DuplicateEntry, + ConcurrencyConflict, +} + +public readonly partial struct CrudResult : IResult +{ + public bool IsFail { get; } + public bool IsOk => !IsFail; + + public CrudError Error => IsFail ? error!.Value : throw new ResultOkException(); + private readonly CrudError? error; + + public string Message => IsFail ? message! : throw new ResultOkException(); + private readonly string? message; + + private CrudResult(bool isFail, CrudError? error, string? message) + { + IsFail = isFail; + this.error = error; + this.message = message; + } + + ///

+ /// Create a successful result + /// + public static CrudResult Ok() => new CrudResult(false, null, null); + + /// + /// Create a failed result with the given error type and message + /// + /// Describes the error type + /// Optional error message + /// + public static CrudResult Fail(string? message = null) => new CrudResult(true, CrudError.Unknown, message); + + internal static CrudResult Fail_Internal(CrudError error, string? message = null) => new CrudResult(true, error, message); + + public static CrudResult DuplicateEntry() + => new CrudResult(true, CrudError.DuplicateEntry, "A similar entry does already exist."); + + public static CrudResult DuplicateEntry(TValue duplicateValue) + => new CrudResult(true, CrudError.DuplicateEntry, $"Entry with value {duplicateValue} does already exist."); + + public static CrudResult NotFound(TKey id) + where TKey : notnull + => new CrudResult(true, CrudError.DuplicateEntry, id.ToString()); + + public static CrudResult ConcurrencyConflict() + => new CrudResult(true, CrudError.DuplicateEntry, "Entry was changed, check updated values."); + + /// + /// Create a successful result + /// Helper to construct a CrudResult without the need to explicit define the generic T + /// + /// Type of the return value + /// Return value + /// + public static CrudResult Ok(T value) => new CrudResult(false, null, null, value); + + internal static CrudResult Fail_Internal(CrudError error, string? message = null) => new CrudResult(true, error, message, default); + + public override string ToString() => IsFail ? $"Failure. Error: {Error}" : $"Success"; +} + +public readonly partial struct CrudResult +{ + public bool IsFail { get; } + public bool IsOk => !IsFail; + + public CrudError Error => IsFail ? error!.Value : throw new ResultOkException(); + private readonly CrudError? error; + + public string Message => IsFail ? message! : throw new ResultOkException(); + private readonly string? message; + + public T Value => IsOk ? value! : throw new ResultFailException(error!.Value.ToString()); + private readonly T? value; + + // A result should be constructed using the static CrudResult.Ok and CrudResult.Fail methods + internal CrudResult(bool isFail, CrudError? error, string? message, T? value) + { + IsFail = isFail; + this.error = error; + this.message = message; + this.value = value; + } + + // Implicit cast from generic value (if given value is also a result, returns a copy) + public static implicit operator CrudResult(T value) + { + if (value is CrudResult result) + { + CrudError? resultError = result.IsFail ? result.Error : null; + string? resultMessage = result.IsFail ? result.Message : null; + T? resultValue = result.IsOk ? result.Value : default; + + return new CrudResult(result.IsFail, resultError, resultMessage, resultValue); + } + + return CrudResult.Ok(value); + } + + // Implicit cast to the non generic result version + public static implicit operator CrudResult(CrudResult result) + { + if (result.IsOk) + return CrudResult.Ok(); + else + return CrudResult.Fail_Internal(result.Error, result.Message); + } + + // Implicit cast from the generic result version + public static implicit operator CrudResult(CrudResult result) + { + if (result.IsOk) + throw new ResultOkException("Can not convert from a CrudResult.Ok to a CrudResult.Ok"); + else + return CrudResult.Fail_Internal(result.Error, result.Message); + } + + public override string ToString() => IsFail ? $"Failed to return {typeof(T)}. Error: {Error}" : $"Successfully returned {typeof(T)} with value {Value}"; +} diff --git a/src/DotNetElements.Core/Core/Result/CrudResultExtensions.cs b/src/DotNetElements.Core/Core/Result/CrudResultExtensions.cs new file mode 100644 index 0000000..63b440e --- /dev/null +++ b/src/DotNetElements.Core/Core/Result/CrudResultExtensions.cs @@ -0,0 +1,44 @@ +using System.Linq.Expressions; +using Microsoft.AspNetCore.Http; +using IHttpResult = Microsoft.AspNetCore.Http.IResult; + +namespace DotNetElements.Core; + +public static class CrudResultExtensions +{ + public static IHttpResult MapToHttpResult(this CrudResult crudResult) + { + if (crudResult.IsOk) + return Results.Ok(); + + return MapToFailedHttpResult(crudResult.Error, crudResult.Message); + } + + public static IHttpResult MapToHttpResultWithProjection(this CrudResult crudResult, Expression> projection) + { + if (crudResult.IsOk) + return Results.Ok(projection.Compile().Invoke(crudResult.Value)); + + return MapToFailedHttpResult(crudResult.Error, crudResult.Message); + } + + public static IHttpResult MapToHttpResult(this CrudResult crudResult) + { + if (crudResult.IsOk) + return Results.Ok(crudResult.Value); + + return MapToFailedHttpResult(crudResult.Error, crudResult.Message); + } + + private static IHttpResult MapToFailedHttpResult(CrudError error, string? message) + { + return error switch + { + CrudError.Unknown => Results.Problem(detail: message), + CrudError.NotFound => Results.NotFound(message), + CrudError.DuplicateEntry => Results.Conflict(message), + CrudError.ConcurrencyConflict => Results.Conflict(message), + _ => throw new NotImplementedException() + }; + } +} diff --git a/src/DotNetElements.Core/Core/Result/CrudResultHelper.cs b/src/DotNetElements.Core/Core/Result/CrudResultHelper.cs new file mode 100644 index 0000000..f6cb8ee --- /dev/null +++ b/src/DotNetElements.Core/Core/Result/CrudResultHelper.cs @@ -0,0 +1,64 @@ +namespace DotNetElements.Core; + +public partial struct CrudResult +{ + /// + /// Creates a result whose success/failure reflects the supplied condition. + /// + public static CrudResult OkIf(bool isSuccess, CrudError error, string? message = null) + { + return isSuccess ? Ok() : Fail_Internal(error, message); + } + + /// + /// Creates a result whose success/failure depends on the supplied predicate. + /// + public static CrudResult OkIf(Func predicate, CrudError error, string? message = null) + { + return OkIf(predicate(), error); + } + + /// + /// Creates a result whose success/failure reflects the supplied condition. + /// + public static CrudResult OkIf(bool isSuccess, T value, CrudError error, string? message = null) + { + return isSuccess ? Ok(value) : Fail_Internal(error, message); + } + + /// + /// Creates a result whose success/failure depends on the supplied predicate. + /// + public static CrudResult OkIf(Func predicate, T value, CrudError error, string? message = null) + { + return OkIf(predicate(), value, error, message); + } + + /// + /// Creates a result whose success/failure depends on weather the value is null or not. + /// + public static CrudResult OkIfNotNull(T? value, CrudError error, string? message = null) + { + return OkIf(value is not null, value!, error, message); + } + + /// + /// Creates a result whose success/failure depends on the supplied predicate. + /// + public static async Task OkIfAsync(Func> predicate, CrudError error, string? message = null) + { + bool isSuccess = await predicate(); + + return OkIf(isSuccess, error, message); + } + + /// + /// Creates a result whose success/failure depends on the supplied predicate. + /// + public static async Task> OkIfAsync(Func> predicate, T value, CrudError error, string? message = null) + { + bool isSuccess = await predicate(); + + return OkIf(isSuccess, value, error, message); + } +} diff --git a/src/DotNetElements.Core/Core/Result/Result.cs b/src/DotNetElements.Core/Core/Result/Result.cs index 707ad7b..c1d317f 100644 --- a/src/DotNetElements.Core/Core/Result/Result.cs +++ b/src/DotNetElements.Core/Core/Result/Result.cs @@ -64,7 +64,7 @@ private Result(bool isFail, string? error) public static Result Ok(T value) => new Result(false, null, value); // Create a failed result with the given error - public static Result Fail(string error = "See log file for more info") => new Result(true, error, default); + internal static Result Fail_Internal(string error) => new Result(true, error, default); // Create a failed result from another failed result public static Result Fail(Result failedResult) @@ -128,7 +128,7 @@ public static implicit operator Result(Result result) if (result.IsOk) throw new ResultOkException("Can not convert from a Result.Ok to a Result.Ok"); else - return Result.Fail(result.Error); + return Result.Fail_Internal(result.Error); } public override string ToString() => IsFail ? $"Failed to return {typeof(T)}. Error: {Error}" : $"Successfully returned {typeof(T)} with value {Value}"; diff --git a/src/DotNetElements.Core/Core/Result/ResultHelper.cs b/src/DotNetElements.Core/Core/Result/ResultHelper.cs index 88ac739..cb177ce 100644 --- a/src/DotNetElements.Core/Core/Result/ResultHelper.cs +++ b/src/DotNetElements.Core/Core/Result/ResultHelper.cs @@ -23,7 +23,7 @@ public static Result OkIf(Func predicate, string error) /// public static Result OkIf(bool isSuccess, T value, string error) { - return isSuccess ? Ok(value) : Fail(error); + return isSuccess ? Ok(value) : Fail_Internal(error); } /// diff --git a/test/DotNetElements.Core.Test/DotNetElements.Core.Test.csproj b/test/DotNetElements.Core.Test/DotNetElements.Core.Test.csproj new file mode 100644 index 0000000..813d7ee --- /dev/null +++ b/test/DotNetElements.Core.Test/DotNetElements.Core.Test.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + enable + enable + false + true + + + + true + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/test/DotNetElements.Core.Test/GlobalUsings.cs b/test/DotNetElements.Core.Test/GlobalUsings.cs new file mode 100644 index 0000000..2ed9e37 --- /dev/null +++ b/test/DotNetElements.Core.Test/GlobalUsings.cs @@ -0,0 +1,9 @@ +global using System.ComponentModel.DataAnnotations; + +global using Microsoft.VisualStudio.TestTools.UnitTesting; +global using Microsoft.EntityFrameworkCore; + +global using FluentAssertions; + +global using DotNetElements.Core; +global using DotNetElements.Core.Test.Utils; \ No newline at end of file diff --git a/test/DotNetElements.Core.Test/ReadOnlyRespositoryTest.cs b/test/DotNetElements.Core.Test/ReadOnlyRespositoryTest.cs new file mode 100644 index 0000000..7d82d0f --- /dev/null +++ b/test/DotNetElements.Core.Test/ReadOnlyRespositoryTest.cs @@ -0,0 +1,34 @@ +using DotNetElements.Core.Test.TestData; + +namespace DotNetElements.Core.Test; + +[TestClass] +public class ReadOnlyRepositoryTest +{ + [TestMethod] + public async Task CreateAsync_SingleEntity_ReturnsValidEntityWithId() + { + using FakeRepositoryFactory factory = new(); + + using (FakeTagRepository tagRepo = factory.CreateRepository()) + { + CrudResult result = await tagRepo.CreateAsync(FakeEntities.TagOne); + } + + using (FakeTagRepository tagRepo = factory.CreateRepository()) + { + IReadOnlyList result = await tagRepo.GetAllAsync(); + + result.Count.Should().Be(1); + + result[0].Should().BeEquivalentTo(FakeEntities.TagOne, + options => options + .Excluding(entity => entity.Id) + .Excluding(entity => entity.CreatorId) + .Excluding(entity => entity.CreationTime)); + + result[0].Id.Should().NotBeEmpty(); + result[0].CreatorId.Should().Be(FakeCurrentUserProvider.); + } + } +} \ No newline at end of file diff --git a/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPost.cs b/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPost.cs new file mode 100644 index 0000000..6b36b27 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPost.cs @@ -0,0 +1,43 @@ +namespace DotNetElements.Core.Test.TestData; + +[RelatedEntities([nameof(Tags)])] +public class BlogPost : AuditedEntity, IUpdatable, IHasVersion +{ + [SQLStringColumn(Length = 256)] + public string Title { get; private set; } + + private List tags = default!; + + [BackingField(nameof(tags))] + public IReadOnlyList Tags => tags; + + [ConcurrencyCheck] + public Guid Version { get; set; } + + public BlogPost(string title, List tags) + { + Title = title; + this.tags = tags; + } + + public BlogPost(Guid id, string title, List tags, Guid version) + { + Id = id; + Title = title; + this.tags = tags; + Version = version; + } + +#nullable disable + private BlogPost() { } +#nullable enable + + public void Update(EditBlogPostModel from, IAttachRelatedEntity attachRelatedEntity) + { + ArgumentNullException.ThrowIfNull(from); + + Title = from.Title; + + EntityHelper.UpdateRelatedEntities(tags, from.Tags, attachRelatedEntity); + } +} diff --git a/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostModel.cs b/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostModel.cs new file mode 100644 index 0000000..2fe053a --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostModel.cs @@ -0,0 +1,32 @@ +namespace DotNetElements.Core.Test.TestData; + +public class BlogPostModel : VersionedModel +{ + public string Title { get; private init; } + public IReadOnlyList Tags { get; private init; } + + public BlogPostModel(Guid id, string title, IReadOnlyList tags, Guid version) : base(id, version) + { + Title = title; + Tags = tags; + } +} + +public class EditBlogPostModel : VersionedEditModel +{ + public string Title { get; set; } + public List Tags { get; set; } + +#nullable disable + public EditBlogPostModel() : base(Guid.NewGuid()) + { + Tags = []; + } +#nullable enable + + public EditBlogPostModel(BlogPostModel blogPost) : base(blogPost.Id, blogPost.Version) + { + Title = blogPost.Title; + Tags = [.. blogPost.Tags]; + } +} diff --git a/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostRepository.cs b/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostRepository.cs new file mode 100644 index 0000000..0342dbe --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/BlogPostModule/BlogPostRepository.cs @@ -0,0 +1,9 @@ +namespace DotNetElements.Core.Test.TestData; + +public class BlogPostRepository : Repository +{ + public BlogPostRepository(TestDbContext dbContext, ICurrentUserProvider currentUserProvider, TimeProvider timeProvider) + : base(dbContext, currentUserProvider, timeProvider) + { + } +} diff --git a/test/DotNetElements.Core.Test/TestData/BlogPostModule/ManagedBlogPostRepository.cs b/test/DotNetElements.Core.Test/TestData/BlogPostModule/ManagedBlogPostRepository.cs new file mode 100644 index 0000000..09f0719 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/BlogPostModule/ManagedBlogPostRepository.cs @@ -0,0 +1,9 @@ +namespace DotNetElements.Core.Test.TestData; + +public class ManagedBlogPostRepository : ManagedRepository +{ + public ManagedBlogPostRepository(IScopedRepositoryFactory repositoryFactory) + : base(repositoryFactory) + { + } +} diff --git a/test/DotNetElements.Core.Test/TestData/BlogPostModule/MapperExtensions.cs b/test/DotNetElements.Core.Test/TestData/BlogPostModule/MapperExtensions.cs new file mode 100644 index 0000000..8ac83b9 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/BlogPostModule/MapperExtensions.cs @@ -0,0 +1,48 @@ +namespace DotNetElements.Core.Test.TestData; + +public static partial class MapperExtensions +{ + public static BlogPost MapToEntity(this EditBlogPostModel model) + { + return new BlogPost + ( + model.Id, + model.Title, + model.Tags.Select(tag => new Tag(tag.Id)).ToList(), + model.Version + ); + } + + public static BlogPostModel MapToModel(this BlogPost entity) + { + return new BlogPostModel + ( + entity.Id, + entity.Title, + entity.Tags.Select(tag => tag.MapToModel()).ToList(), + entity.Version + ); + } + + public static IQueryable MapToModel(this IQueryable query) + { + return Queryable.Select(query, entity => new BlogPostModel + ( + entity.Id, + entity.Title, + Enumerable.ToList(Enumerable.Select(entity.Tags, tag => new TagModel(tag.Id, tag.Label, tag.Version))), + entity.Version + )); + } + + public static IQueryable> MapToModelWithDetails(this IQueryable query) + { + return Queryable.Select(query, entity => new ModelWithDetails(new BlogPostModel + ( + entity.Id, + entity.Title, + Enumerable.ToList(Enumerable.Select(entity.Tags, tag => new TagModel(tag.Id, tag.Label, tag.Version))), + entity.Version + ))); + } +} diff --git a/test/DotNetElements.Core.Test/TestData/FakeEntities.cs b/test/DotNetElements.Core.Test/TestData/FakeEntities.cs new file mode 100644 index 0000000..fb60880 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/FakeEntities.cs @@ -0,0 +1,12 @@ +namespace DotNetElements.Core.Test.TestData; + +internal static class FakeEntities +{ + public static Tag TagOne => new Tag("Test Tag 1"); + public static Tag TagTwo => new Tag("Test Tag 2"); + public static Tag TagThree => new Tag("Test Tag 3"); + + public static BlogPost BlogPostOne => new BlogPost("Test BlogPost 1", []); + public static BlogPost BlogPostTwo => new BlogPost("Test BlogPost 2", []); + public static BlogPost BlogPostThree => new BlogPost("Test BlogPost 3", []); +} diff --git a/test/DotNetElements.Core.Test/TestData/TagModule/FakeTagRepository.cs b/test/DotNetElements.Core.Test/TestData/TagModule/FakeTagRepository.cs new file mode 100644 index 0000000..34adcc8 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/TagModule/FakeTagRepository.cs @@ -0,0 +1,14 @@ +namespace DotNetElements.Core.Test.TestData; + +internal class FakeTagRepository : FakeRepository +{ + public FakeTagRepository(TestDbContext dbContext, ICurrentUserProvider currentUserProvider, TimeProvider timeProvider) + : base(dbContext, currentUserProvider, timeProvider) + { + } + + public FakeTagRepository(TestDbContext dbContext) + : base(dbContext) + { + } +} diff --git a/test/DotNetElements.Core.Test/TestData/TagModule/ManagedTagRepository.cs b/test/DotNetElements.Core.Test/TestData/TagModule/ManagedTagRepository.cs new file mode 100644 index 0000000..938bdc9 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/TagModule/ManagedTagRepository.cs @@ -0,0 +1,9 @@ +namespace DotNetElements.Core.Test.TestData; + +internal class ManagedTagRepository : ManagedRepository +{ + public ManagedTagRepository(IScopedRepositoryFactory repositoryFactory) + : base(repositoryFactory) + { + } +} diff --git a/test/DotNetElements.Core.Test/TestData/TagModule/MapperExtensions.cs b/test/DotNetElements.Core.Test/TestData/TagModule/MapperExtensions.cs new file mode 100644 index 0000000..97b3aa2 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/TagModule/MapperExtensions.cs @@ -0,0 +1,44 @@ +namespace DotNetElements.Core.Test.TestData; + +public static partial class MapperExtensions +{ + public static Tag MapToEntity(this EditTagModel model) + { + return new Tag + ( + model.Id, + model.Label, + model.Version + ); + } + + public static TagModel MapToModel(this Tag entity) + { + return new TagModel + ( + entity.Id, + entity.Label, + entity.Version + ); + } + + public static IQueryable MapToModel(this IQueryable query) + { + return Queryable.Select(query, entity => new TagModel + ( + entity.Id, + entity.Label, + entity.Version + )); + } + + public static IQueryable> MapToModelWithDetails(this IQueryable query) + { + return Queryable.Select(query, entity => new ModelWithDetails(new TagModel + ( + entity.Id, + entity.Label, + entity.Version + ))); + } +} diff --git a/test/DotNetElements.Core.Test/TestData/TagModule/Tag.cs b/test/DotNetElements.Core.Test/TestData/TagModule/Tag.cs new file mode 100644 index 0000000..81db05e --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/TagModule/Tag.cs @@ -0,0 +1,48 @@ +namespace DotNetElements.Core.Test.TestData; + +public class Tag : AuditedEntity, IUpdatable, IRelatedEntity, IHasVersion +{ + [SQLStringColumn(Length = 256)] + public string Label { get; private set; } + + private readonly List blogPosts = default!; + + [BackingField(nameof(blogPosts))] + public IReadOnlyList BlogPosts => blogPosts; + + [ConcurrencyCheck] + public Guid Version { get; set; } + + public Tag(string label) + { + Label = label; + } + + public Tag(Guid id, string label, Guid version) + { + Id = id; + Label = label; + Version = version; + } + +#nullable disable + public Tag(Guid id) + { + Id = id; + } + + private Tag() { } +#nullable enable + + public void Update(EditTagModel from, IAttachRelatedEntity _) + { + ArgumentNullException.ThrowIfNull(from); + + Label = from.Label; + } + + public static Tag CreateRefById(Guid id) + { + return new Tag(id); + } +} diff --git a/test/DotNetElements.Core.Test/TestData/TagModule/TagModel.cs b/test/DotNetElements.Core.Test/TestData/TagModule/TagModel.cs new file mode 100644 index 0000000..36f4a9b --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/TagModule/TagModel.cs @@ -0,0 +1,27 @@ +namespace DotNetElements.Core.Test.TestData; + +public class TagModel : VersionedModel +{ + public string Label { get; private init; } + + public TagModel(Guid id, string label, Guid version) : base(id, version) + { + Label = label; + } + + public override string ToString() => Label; +} + +public class EditTagModel : VersionedEditModel +{ + public string Label { get; set; } + +#nullable disable + public EditTagModel() : base(Guid.NewGuid()) { } +#nullable enable + + public EditTagModel(TagModel tag) : base(tag.Id, tag.Version) + { + Label = tag.Label; + } +} diff --git a/test/DotNetElements.Core.Test/TestData/TestDbContext.cs b/test/DotNetElements.Core.Test/TestData/TestDbContext.cs new file mode 100644 index 0000000..296fe32 --- /dev/null +++ b/test/DotNetElements.Core.Test/TestData/TestDbContext.cs @@ -0,0 +1,13 @@ +namespace DotNetElements.Core.Test.TestData; + +public class TestDbContext : DbContext +{ + public DbSet BlogPosts { get; set; } + + public DbSet Tags { get; set; } + + public TestDbContext(DbContextOptions options) : base(options) + { + + } +} diff --git a/test/DotNetElements.Core.Test/Utils/FakeCurrentUserProvider.cs b/test/DotNetElements.Core.Test/Utils/FakeCurrentUserProvider.cs new file mode 100644 index 0000000..365f054 --- /dev/null +++ b/test/DotNetElements.Core.Test/Utils/FakeCurrentUserProvider.cs @@ -0,0 +1,14 @@ + +namespace DotNetElements.Core.Test.Utils; + +internal class FakeCurrentUserProvider : ICurrentUserProvider +{ + public static readonly Guid FakeUserIdOne = new Guid("DC0BA927-FBAE-4DCA-8BAE-C1C70CBB948D"); + public static readonly Guid FakeUserIdTwo = new Guid("65FA2034-6544-43E3-AF5C-DF311AE1B076"); + + private Guid currentUser = FakeUserIdOne; + + public void SetCurrentUserId(Guid user) => currentUser = user; + + public Guid GetCurrentUserId() => currentUser; +} diff --git a/test/DotNetElements.Core.Test/Utils/FakeDbContextFactory.cs b/test/DotNetElements.Core.Test/Utils/FakeDbContextFactory.cs new file mode 100644 index 0000000..857e95d --- /dev/null +++ b/test/DotNetElements.Core.Test/Utils/FakeDbContextFactory.cs @@ -0,0 +1,42 @@ +using System.Data.Common; +using Microsoft.Data.Sqlite; + +namespace DotNetElements.Core.Test.Utils; + +public sealed class FakeDbContextFactory : IDisposable + where TDbContext : DbContext +{ + private DbConnection? connection; + + private DbContextOptions CreateOptions() + { + ArgumentNullException.ThrowIfNull(connection); + + return new DbContextOptionsBuilder() + .UseSqlite(connection).Options; + } + + public TDbContext CreateContext() + { + if (connection is null) + { + connection = new SqliteConnection("DataSource=:memory:"); + connection.Open(); + + using TDbContext context = (TDbContext)Activator.CreateInstance(typeof(TDbContext), CreateOptions())!; + + context.Database.EnsureCreated(); + } + + return (TDbContext)Activator.CreateInstance(typeof(TDbContext), CreateOptions())!; + } + + public void Dispose() + { + if (connection is not null) + { + connection.Dispose(); + connection = null; + } + } +} diff --git a/test/DotNetElements.Core.Test/Utils/FakeRepositoryFactory.cs b/test/DotNetElements.Core.Test/Utils/FakeRepositoryFactory.cs new file mode 100644 index 0000000..54a24e3 --- /dev/null +++ b/test/DotNetElements.Core.Test/Utils/FakeRepositoryFactory.cs @@ -0,0 +1,20 @@ +namespace DotNetElements.Core.Test.Utils; + +internal sealed class FakeRepositoryFactory : IDisposable + where TDbContext : DbContext + where TRepository : FakeRepository + where TEntity : Entity + where TKey : notnull +{ + private readonly FakeDbContextFactory dbContextFactory = new(); + + public TRepository CreateRepository() + { + return (TRepository)Activator.CreateInstance(typeof(TRepository), dbContextFactory.CreateContext())!; + } + + public void Dispose() + { + dbContextFactory.Dispose(); + } +} diff --git a/test/DotNetElements.Core.Test/Utils/RakeRepository.cs b/test/DotNetElements.Core.Test/Utils/RakeRepository.cs new file mode 100644 index 0000000..3125f7d --- /dev/null +++ b/test/DotNetElements.Core.Test/Utils/RakeRepository.cs @@ -0,0 +1,28 @@ +using Microsoft.Extensions.Time.Testing; + +namespace DotNetElements.Core.Test.Utils; + +internal class FakeRepository : Repository, IDisposable + where TDbContext : DbContext + where TEntity : Entity + where TKey : notnull +{ + public FakeRepository(TDbContext dbContext) : base( + dbContext, + new FakeCurrentUserProvider(), + new FakeTimeProvider()) + { + } + + public FakeRepository(TDbContext dbContext, ICurrentUserProvider currentUserProvider, TimeProvider timeProvider) : base( + dbContext, + currentUserProvider, + timeProvider) + { + } + + public void Dispose() + { + DbContext.Dispose(); + } +} From 8d6a4dab9c549ae50f13d425a4d677cd1c11d454 Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber <55654328+Felix-CodingClimber@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:50:26 +0100 Subject: [PATCH 2/6] Create README.md --- README.md | 225 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d657d4 --- /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/) + + +
+
+ + Logo + + +

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 +
    +
  1. + About The Project + +
  2. + +
  3. License
  4. + +
+
+ + + + +## 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/ + + From c695d2c1be2848c845302b943c1fcf1020eb5a06 Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber <55654328+Felix-CodingClimber@users.noreply.github.com> Date: Mon, 15 Jan 2024 15:53:14 +0100 Subject: [PATCH 3/6] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6d657d4..bcb49ee 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ ### Built With -* [![NET][.NET]][.NET-url] +[![NET][.NET]][.NET-url]

(back to top)

From b5668776a506d8b91882e8b84af95413e795a6dd Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber <55654328+Felix-CodingClimber@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:00:06 +0100 Subject: [PATCH 4/6] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE 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. From d7c4d64c75143da265d2b4fb12cf578df35ebf68 Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber <55654328+Felix-CodingClimber@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:00:22 +0100 Subject: [PATCH 5/6] Rename LICENSE to LICENSE.md --- LICENSE => LICENSE.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE => LICENSE.md (100%) diff --git a/LICENSE b/LICENSE.md similarity index 100% rename from LICENSE rename to LICENSE.md From 4b2ee6260c836e5157fb828f0e248d810571f9d8 Mon Sep 17 00:00:00 2001 From: Felix-CodingClimber <55654328+Felix-CodingClimber@users.noreply.github.com> Date: Mon, 15 Jan 2024 16:00:38 +0100 Subject: [PATCH 6/6] Rename LICENSE.md to LICENSE --- LICENSE.md => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE.md => LICENSE (100%) diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE