diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4fc74f8..4a3666b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ jobs: VsixManifestSourcePath: src\source.extension.cs steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup .NET build dependencies uses: timheuer/bootstrap-dotnet@v1 @@ -47,7 +47,7 @@ jobs: # run: vstest.console.exe _built\*test.dll - name: Upload artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: CleanArchitectureCodeGenerator.vsix path: _built/**/*.vsix @@ -57,10 +57,10 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Download Package artifact - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v4 with: name: CleanArchitectureCodeGenerator.vsix diff --git a/LICENSE b/LICENSE index 2efeed0..dfe5817 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,21 @@ -Copyright 2014 Mads Kristensen +MIT License -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at +Copyright (c) CleanArchitectureCodeGenerator - http://www.apache.org/licenses/LICENSE-2.0 +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: -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +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 index 3fcabed..58003c1 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ -# Code Generator for Clean Architecture +# Speed up your Clean Architecture development in Visual Studio! [![Build](https://github.com/neozhu/CleanArchitectureCodeGenerator/actions/workflows/build.yml/badge.svg)](https://github.com/neozhu/CleanArchitectureCodeGenerator/actions/workflows/build.yml) ![Visual Studio Marketplace Version (including pre-releases)](https://img.shields.io/visual-studio-marketplace/v/neozhu.247365) ![Visual Studio Marketplace Downloads](https://img.shields.io/visual-studio-marketplace/d/neozhu.247365?label=Downloads) +[Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) is a software design philosophy that separates the elements of a design into ring levels. The main rule of Clean Architecture is that code dependencies can only move from the outer levels inward. Code on the inner layers can have no knowledge of functions on the outer layers. This extension helps you generate code that adheres to this principle. ## Download the extension @@ -28,24 +29,36 @@ See the [changelog](CHANGELOG.md) for updates and roadmap. ### Features -- Easily create following application features code -- {nameofPlural}/Commands/AddEdit/AddEdit{name}Command.cs -- {nameofPlural}/Commands/AddEdit/AddEdit{name}CommandValidator.cs -- {nameofPlural}/Commands/Create/Create{name}Command.cs -- {nameofPlural}/Commands/Create/Create{name}CommandValidator.cs -- {nameofPlural}/Commands/Delete/Delete{name}Command.cs -- {nameofPlural}/Commands/Delete/Delete{name}CommandValidator.cs -- {nameofPlural}/Commands/Update/Update{name}Command.cs -- {nameofPlural}/Commands/Update/Update{name}CommandValidator.cs -- {nameofPlural}/Commands/Import/Import{name}Command.cs -- {nameofPlural}/Commands/Import/Import{name}CommandValidator.cs -- {nameofPlural}/DTOs/{name}Dto.cs -- {nameofPlural}/EventHandlers/{name}CreatedEventHandler.cs -- {nameofPlural}/EventHandlers/{name}UpdatedEventHandler.cs -- {nameofPlural}/EventHandlers/{name}DeletedEventHandler.cs -- {nameofPlural}/Queries/Export/Export{nameofPlural}Query.cs -- {nameofPlural}/Queries/GetAll/GetAll{nameofPlural}Query.cs -- {nameofPlural}/Queries/Pagination/{nameofPlural}PaginationQuery.cs +This extension helps you rapidly scaffold components for your Clean Architecture project: + +#### Core Application Layer Components +Quickly generate essential C# classes for your application layer, including: + +* **Commands and Validators:** For operations that change the state of your application (Add/Edit, Create, Delete, Update, Import). + * `{nameofPlural}/Commands/AddEdit/AddEdit{name}Command.cs` + * `{nameofPlural}/Commands/AddEdit/AddEdit{name}CommandValidator.cs` + * `{nameofPlural}/Commands/Create/Create{name}Command.cs` + * `{nameofPlural}/Commands/Create/Create{name}CommandValidator.cs` + * `{nameofPlural}/Commands/Delete/Delete{name}Command.cs` + * `{nameofPlural}/Commands/Delete/Delete{name}CommandValidator.cs` + * `{nameofPlural}/Commands/Update/Update{name}Command.cs` + * `{nameofPlural}/Commands/Update/Update{name}CommandValidator.cs` + * `{nameofPlural}/Commands/Import/Import{name}Command.cs` + * `{nameofPlural}/Commands/Import/Import{name}CommandValidator.cs` +* **Data Transfer Objects (DTOs):** To define how data is sent and received. + * `{nameofPlural}/DTOs/{name}Dto.cs` +* **Event Handlers:** For domain events (Created, Updated, Deleted). + * `{nameofPlural}/EventHandlers/{name}CreatedEventHandler.cs` + * `{nameofPlural}/EventHandlers/{name}UpdatedEventHandler.cs` + * `{nameofPlural}/EventHandlers/{name}DeletedEventHandler.cs` +* **Queries:** For retrieving data (Export, GetAll, Pagination). + * `{nameofPlural}/Queries/Export/Export{nameofPlural}Query.cs` + * `{nameofPlural}/Queries/GetAll/GetAll{nameofPlural}Query.cs` + * `{nameofPlural}/Queries/Pagination/{nameofPlural}PaginationQuery.cs` + +#### TypeScript Definition Generation +Automatically generate TypeScript definition files (`.d.ts`) for your Data Transfer Objects (DTOs), enabling type-safe interaction with your frontend applications. +* `{nameofPlural}/DTOs/{name}Dto.d.ts` ### CleanArchitecture for Blazor Server Application project Please use this in collaboration with this project. @@ -94,5 +107,6 @@ to install the extension for Visual Studio which enables some features used by this project. -## License -[Apache 2.0](LICENSE) +## **License** + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/src/CleanArchitectureCodeGenerator.csproj b/src/CleanArchitectureCodeGenerator.csproj index c3d3979..b74513e 100644 --- a/src/CleanArchitectureCodeGenerator.csproj +++ b/src/CleanArchitectureCodeGenerator.csproj @@ -111,6 +111,18 @@ source.extension.vsixmanifest + + true + + + true + + + true + + + true + true @@ -138,7 +150,7 @@ true - + true @@ -222,7 +234,7 @@ true - + true @@ -279,20 +291,26 @@ + + 6.1.0 + - 17.4.33103.184 + 17.13.40008 compile; build; native; contentfiles; analyzers; buildtransitive - 17.0.65 + 17.8.8 - 17.5.4065 + 17.13.2126 runtime; build; native; contentfiles; analyzers; buildtransitive all - 7.0.0 + 9.0.3 + + + 9.0.3 diff --git a/src/CodeGeneratorPackage.cs b/src/CodeGeneratorPackage.cs index 0e19365..9b8c295 100644 --- a/src/CodeGeneratorPackage.cs +++ b/src/CodeGeneratorPackage.cs @@ -27,13 +27,18 @@ namespace CleanArchitecture.CodeGenerator [Guid(PackageGuids.guidCodeGeneratorPkgString)] public sealed class CodeGeneratorPackage : AsyncPackage { + public const string DOMAINPROJECT = "Domain"; + public const string UIPROJECT = "Server.UI"; + public const string INFRASTRUCTUREPROJECT = "Infrastructure"; + public const string APPLICATIONPROJECT = "Application"; + private const string _solutionItemsProjectName = "Solution Items"; private static readonly Regex _reservedFileNamePattern = new Regex($@"(?i)^(PRN|AUX|NUL|CON|COM\d|LPT\d)(\.|$)"); private static readonly HashSet _invalidFileNameChars = new HashSet(Path.GetInvalidFileNameChars()); public static DTE2 _dte; - protected async override System.Threading.Tasks.Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) + protected async override Task InitializeAsync(CancellationToken cancellationToken, IProgress progress) { await JoinableTaskFactory.SwitchToMainThreadAsync(); @@ -55,15 +60,17 @@ private void ExecuteAsync(object sender, EventArgs e) NewItemTarget target = NewItemTarget.Create(_dte); NewItemTarget domain= NewItemTarget.Create(_dte,"Domain"); NewItemTarget infrastructure = NewItemTarget.Create(_dte, "Infrastructure"); - NewItemTarget ui = NewItemTarget.Create(_dte, "Blazor.Server.UI"); - var includes = new string[] { "IEntity", "BaseEntity", "BaseAuditableEntity", "BaseAuditableSoftDeleteEntity", "AuditTrail", "OwnerPropertyEntity" }; + NewItemTarget ui = NewItemTarget.Create(_dte, "Server.UI"); + var includes = new string[] { "IEntity", "BaseEntity", "BaseAuditableEntity", "BaseAuditableSoftDeleteEntity", "AuditTrail", "OwnerPropertyEntity","KeyValue" }; + + var testlist = ProjectHelpers.GetEntities(domain.Project); var objectlist = ProjectHelpers.GetEntities(domain.Project) - .Where(x => includes.Contains(x.BaseName) && !includes.Contains(x.Name)); - var entities = objectlist.Select(x=>x.Name).Distinct().ToArray(); - if (target == null) + .Where(x => x.IsEnum || (includes.Contains(x.BaseName) && !includes.Contains(x.Name))); + var entities = objectlist.Where(x=>x.IsEnum==false).Select(x=>x.Name).ToArray(); + if (target == null && target.Project.Name == APPLICATIONPROJECT) { MessageBox.Show( - "Could not determine where to create the new file. Select a file or folder in Solution Explorer and try again.", + "Unable to determine the location for creating the new file. Please select a folder within the Application Project in the Explorer and try again.", Vsix.Name, MessageBoxButton.OK, MessageBoxImage.Error); @@ -93,14 +100,15 @@ private void ExecuteAsync(object sender, EventArgs e) }; foreach (var item in events) { - AddItemAsync(objectClass,item, name, domain).Forget(); + AddItemAsync(objectClass,item, name, domain, objectlist).Forget(); } var configurations = new List() { - $"Persistence/Configurations/{name}Configuration.cs" + $"Persistence/Configurations/{name}Configuration.cs", + $"PermissionSet/{nameofPlural}.cs" }; foreach (var item in configurations) { - AddItemAsync(objectClass, item, name, infrastructure).Forget(); + AddItemAsync(objectClass, item, name, infrastructure, objectlist).Forget(); } var list = new List() @@ -127,22 +135,26 @@ private void ExecuteAsync(object sender, EventArgs e) $"{nameofPlural}/Queries/GetAll/GetAll{nameofPlural}Query.cs", $"{nameofPlural}/Queries/GetById/Get{name}ByIdQuery.cs", $"{nameofPlural}/Queries/Pagination/{nameofPlural}PaginationQuery.cs", - + $"{nameofPlural}/Security/{nameofPlural}Permissions.cs", + }; foreach (var item in list) { - AddItemAsync(objectClass,item, name, target).Forget(); + AddItemAsync(objectClass,item, name, target, objectlist).Forget(); } var pages = new List() { + $"Pages/{nameofPlural}/Create{name}.razor", + $"Pages/{nameofPlural}/Edit{name}.razor", + $"Pages/{nameofPlural}/View{name}.razor", $"Pages/{nameofPlural}/{nameofPlural}.razor", - $"Pages/{nameofPlural}/_{name}FormDialog.razor", - $"Pages/{nameofPlural}/Components/{nameofPlural}AdvancedSearchComponent.razor" + $"Pages/{nameofPlural}/Components/{name}FormDialog.razor", + //$"Pages/{nameofPlural}/Components/{nameofPlural}AdvancedSearchComponent.razor" }; foreach (var item in pages) { - AddItemAsync(objectClass,item, name, ui).Forget(); + AddItemAsync(objectClass,item, name, ui, objectlist).Forget(); } } @@ -158,7 +170,7 @@ private void ExecuteAsync(object sender, EventArgs e) } } - private async Task AddItemAsync(IntellisenseObject classObject, string name,string itemname, NewItemTarget target) + private async Task AddItemAsync(IntellisenseObject classObject, string name,string itemname, NewItemTarget target,IEnumerable objectlist=null) { // The naming rules that apply to files created on disk also apply to virtual solution folders, // so regardless of what type of item we are creating, we need to validate the name. @@ -178,7 +190,7 @@ private async Task AddItemAsync(IntellisenseObject classObject, string name,stri } else { - await AddFileAsync(classObject,name, itemname, target); + await AddFileAsync(classObject,name, itemname, target, objectlist); } } @@ -202,7 +214,7 @@ private void ValidatePath(string path) } while (!string.IsNullOrEmpty(path)); } - private async Task AddFileAsync(IntellisenseObject classObject, string name,string itemname, NewItemTarget target) + private async Task AddFileAsync(IntellisenseObject classObject, string name,string itemname, NewItemTarget target,IEnumerable objectlist=null) { await JoinableTaskFactory.SwitchToMainThreadAsync(); FileInfo file; @@ -236,7 +248,7 @@ private async Task AddFileAsync(IntellisenseObject classObject, string name,stri project = target.Project; } - int position = await WriteFileAsync(project, classObject, file.FullName, itemname, target.Directory); + int position = await WriteFileAsync(project, classObject, file.FullName, itemname, target.Directory, objectlist); if (target.ProjectItem != null && target.ProjectItem.IsKind(Constants.vsProjectItemKindVirtualFolder)) { target.ProjectItem.ProjectItems.AddFromFile(file.FullName); @@ -269,9 +281,9 @@ private async Task AddFileAsync(IntellisenseObject classObject, string name,stri } } - private static async Task WriteFileAsync(Project project, IntellisenseObject classObject, string file,string itemname,string selectFolder) + private static async Task WriteFileAsync(Project project, IntellisenseObject classObject, string file,string itemname,string selectFolder,IEnumerable objectlist=null) { - string template = await TemplateMap.GetTemplateFilePathAsync(project, classObject,file, itemname, selectFolder); + string template = await TemplateMap.GetTemplateFilePathAsync(project, classObject,file, itemname, selectFolder, objectlist); if (!string.IsNullOrEmpty(template)) { @@ -291,7 +303,7 @@ private static async Task WriteFileAsync(Project project, IntellisenseObjec return 0; } - private static async System.Threading.Tasks.Task WriteToDiskAsync(string file, string content) + private static async Task WriteToDiskAsync(string file, string content) { using (StreamWriter writer = new StreamWriter(file, false, GetFileEncoding(file))) { diff --git a/src/Models/IntellisenseParser.cs b/src/Models/IntellisenseParser.cs index 472355d..f7aae98 100644 --- a/src/Models/IntellisenseParser.cs +++ b/src/Models/IntellisenseParser.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Numerics; using System.Runtime.InteropServices; using System.Text.RegularExpressions; using System.Xml.Linq; @@ -90,7 +91,7 @@ private static void ProcessElement(CodeElement element, List .FirstOrDefault(c => c.FullName != "System.Object"); } catch { /* Silently continue. */ } - var baseClasses = new string[] { "BaseAuditableSoftDeleteEntity", "BaseAuditableEntity", "BaseEntity", "IEntity", "ISoftDelete" }; + var baseClasses = new string[] { "BaseAuditableSoftDeleteEntity", "BaseAuditableEntity", "BaseEntity", "IEntity", "ISoftDelete", "OwnerPropertyEntity" }; if (baseClass != null && baseClasses.Contains(GetClassName(baseClass))) { ProcessClass(cc, baseClass, list, underProcess); @@ -401,6 +402,13 @@ private static CodeTypeRef TryToGuessGenericArgument(CodeClass rootElement, Code { "float", typeof( float ) }, { "double", typeof( double ) }, { "decimal", typeof( decimal ) }, + { "Guid", typeof( Guid ) }, + { "DateTime", typeof( DateTime ) }, + { "DateTimeOffset", typeof( DateTimeOffset ) }, + { "bool", typeof( bool ) }, + { "BigInteger", typeof( BigInteger ) }, + { "char", typeof( char ) } + }; private static string TryToGuessFullName(string typeName) diff --git a/src/Templatemap.cs b/src/Templatemap.cs index 21e1907..10e86c1 100644 --- a/src/Templatemap.cs +++ b/src/Templatemap.cs @@ -34,11 +34,11 @@ static TemplateMap() _folder = Path.Combine(Path.GetDirectoryName(assembly), "Templates"); _templateFiles.AddRange(Directory.GetFiles(_folder, "*" + _defaultExt, SearchOption.AllDirectories)); } - - public static async Task GetTemplateFilePathAsync(Project project, IntellisenseObject classObject, string file,string itemname,string selectFolder) + + public static async Task GetTemplateFilePathAsync(Project project, IntellisenseObject classObject, string file, string itemname, string selectFolder, IEnumerable objectlist = null) { - var templatefolders =new string[]{ + var templatefolders = new string[]{ "Commands\\AcceptChanges", "Commands\\Create", "Commands\\Delete", @@ -46,6 +46,7 @@ public static async Task GetTemplateFilePathAsync(Project project, Intel "Commands\\AddEdit", "Commands\\Import", "DTOs", + /*"Mappers",*/ "Caching", "EventHandlers", "Events", @@ -57,6 +58,7 @@ public static async Task GetTemplateFilePathAsync(Project project, Intel "Pages", "Pages\\Components", "Persistence\\Configurations", + "Security", }; var extension = Path.GetExtension(file).ToLowerInvariant(); var name = Path.GetFileName(file); @@ -70,18 +72,18 @@ public static async Task GetTemplateFilePathAsync(Project project, Intel // Look for direct file name matches if (list.Any(f => { - var pattern = templatefolders.Where(x => relative.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0).First().Replace("\\","\\\\"); - var result = Regex.IsMatch(f, pattern, RegexOptions.IgnoreCase); - return result; - - }) ) + var pattern = templatefolders.Where(x => relative.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0).First().Replace("\\", "\\\\"); + var result = Regex.IsMatch(f, pattern, RegexOptions.IgnoreCase); + return result; + + })) { - var tmplFile = list.OrderByDescending(x=>x.Length).FirstOrDefault(f => { + var tmplFile = list.OrderByDescending(x => x.Length).FirstOrDefault(f => { var pattern = templatefolders.Where(x => relative.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0).First().Replace("\\", "\\\\"); ; var result = Regex.IsMatch(f, pattern, RegexOptions.IgnoreCase); if (result) { - return Path.GetFileNameWithoutExtension(f).Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).All(x=>name.IndexOf(x, StringComparison.OrdinalIgnoreCase)>=0); + return Path.GetFileNameWithoutExtension(f).Split(new char[] { '.' }, StringSplitOptions.RemoveEmptyEntries).All(x => name.IndexOf(x, StringComparison.OrdinalIgnoreCase) >= 0); } return false; }); @@ -95,7 +97,7 @@ public static async Task GetTemplateFilePathAsync(Project project, Intel var tmpl = AdjustForSpecific(safeName, extension); templateFile = Path.Combine(Path.GetDirectoryName(tmplFile), tmpl + _defaultExt); //GetTemplate(tmpl); } - var template = await ReplaceTokensAsync(project, classObject, itemname, relative, selectRelative, templateFile); + var template = await ReplaceTokensAsync(project, classObject, itemname, relative, selectRelative, templateFile, objectlist); return NormalizeLineEndings(template); } @@ -116,7 +118,7 @@ private static void AddTemplatesFromCurrentFolder(List list, string dir) list.InsertRange(0, dynaList); } - private static async Task ReplaceTokensAsync(Project project, IntellisenseObject classObject, string name, string relative,string selectRelative, string templateFile) + private static async Task ReplaceTokensAsync(Project project, IntellisenseObject classObject, string name, string relative, string selectRelative, string templateFile, IEnumerable objectlist = null) { if (string.IsNullOrEmpty(templateFile)) { @@ -139,28 +141,49 @@ private static async Task ReplaceTokensAsync(Project project, Intellisen { var content = await reader.ReadToEndAsync(); var nameofPlural = ProjectHelpers.Pluralize(name); - var dtoFieldDefinition = createDtoFieldDefinition(classObject); - var importFuncExpression = createImportFuncExpression(classObject); + var dtoFieldDefinition = createDtoFieldDefinition(classObject, objectlist); + var dtoFieldDefinitionWithoutList = createDtoFieldDefinitionWithoutList(classObject, objectlist); + var importFuncExpression = createImportFuncExpression(classObject, objectlist); var templateFieldDefinition = createTemplateFieldDefinition(classObject); - var exportFuncExpression = createExportFuncExpression(classObject); - var mudTdDefinition = createMudTdDefinition(classObject); - var mudTdHeaderDefinition = createMudTdHeaderDefinition(classObject); - var mudFormFieldDefinition = createMudFormFieldDefinition(classObject); - var fieldAssignmentDefinition = createFieldAssignmentDefinition(classObject); - return content.Replace("{rootnamespace}", _defaultNamespace) - .Replace("{namespace}", ns) - .Replace("{selectns}", selectNs) - .Replace("{itemname}", name) - .Replace("{nameofPlural}", nameofPlural) - .Replace("{dtoFieldDefinition}", dtoFieldDefinition) - .Replace("{fieldAssignmentDefinition}", fieldAssignmentDefinition) - .Replace("{importFuncExpression}", importFuncExpression) - .Replace("{templateFieldDefinition}", templateFieldDefinition) - .Replace("{exportFuncExpression}", exportFuncExpression) - .Replace("{mudTdDefinition}", mudTdDefinition) - .Replace("{mudTdHeaderDefinition}", mudTdHeaderDefinition) - .Replace("{mudFormFieldDefinition}", mudFormFieldDefinition) - ; + var exportFuncExpression = createExportFuncExpression(classObject, objectlist); + var mudTdDefinition = createMudTdDefinition(name,classObject); + var mudTdHeaderDefinition = createMudTdHeaderDefinition(name,classObject, objectlist); + var mudFormFieldDefinition = createMudFormFieldDefinition(classObject, objectlist); + var readonlyFieldDefinition = createReadyonlyFieldDefinition(classObject, objectlist); + var fieldAssignmentDefinition = createFieldAssignmentDefinition(classObject, objectlist); + var entityTypeBuilderConfirmation = createEntityTypeBuilderConfirmation(classObject, objectlist); + var commandValidatorRuleFor = createComandValidatorRuleFor(classObject, objectlist); + var replacements = new Dictionary + { + { "rootnamespace", _defaultNamespace }, + { "namespace", ns }, + { "selectns", selectNs }, + { "author", "neozhu"}, + { "createddate", DateTime.Now.ToString("yyyy-MM-dd") }, + { "itemname", name }, + { "itemnamelowercase", name.ToLower() }, + { "nameofPlural", nameofPlural }, + { "nameofplurallowercase", nameofPlural.ToLower() }, + { "dtoFieldDefinition", dtoFieldDefinition }, + { "dtoFieldDefinitionWithoutList", dtoFieldDefinitionWithoutList }, + { "fieldAssignmentDefinition", fieldAssignmentDefinition }, + { "importFuncExpression", importFuncExpression }, + { "templateFieldDefinition", templateFieldDefinition }, + { "exportFuncExpression", exportFuncExpression }, + { "mudTdDefinition", mudTdDefinition }, + { "mudTdHeaderDefinition", mudTdHeaderDefinition }, + { "mudFormFieldDefinition", mudFormFieldDefinition }, + { "readonlyFieldDefinition", readonlyFieldDefinition }, + { "entityTypeBuilderConfirmation", entityTypeBuilderConfirmation }, + { "commandValidatorRuleFor", commandValidatorRuleFor } + }; + + string result = Regex.Replace(content, @"\{(\w+)\}", match => { + string key = match.Groups[1].Value; + return replacements.TryGetValue(key, out var value) ? value : match.Value; + }); + return result; + } } @@ -185,86 +208,303 @@ private static string AdjustForSpecific(string safeName, string extension) } private static string splitCamelCase(string str) { - var r = new Regex(@" - (?<=[A-Z])(?=[A-Z][a-z]) | - (?<=[^A-Z])(?=[A-Z]) | - (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace); - return r.Replace(str, " "); + // Define the regular expression to split the CamelCase string + var r = new Regex(@"(?<=[A-Z])(?=[A-Z][a-z]) | + (?<=[^A-Z])(?=[A-Z]) | + (?<=[A-Za-z])(?=[^A-Za-z])", + RegexOptions.IgnorePatternWhitespace); // Allows formatting with spaces for better readability + + // Use the regular expression to replace matches with a space + var result = r.Replace(str, " "); + + // If the result is not empty, proceed to format it + if (!string.IsNullOrEmpty(result)) + { + // Convert the entire string to lowercase first + result = result.ToLower(); + + // Then capitalize the first character of the string to ensure the first word is properly capitalized + result = char.ToUpper(result[0]) + result.Substring(1); + } + + return result; } + public const string PRIMARYKEY = "Id"; - private static string createDtoFieldDefinition(IntellisenseObject classObject) + private static string createDtoFieldDefinition(IntellisenseObject classObject, IEnumerable objectlist = null) { var output = new StringBuilder(); - foreach(var property in classObject.Properties.Where(x => x.Type.IsKnownType == true)) + foreach (var property in classObject.Properties.Where(x => x.Type.IsDictionary == false)) { - output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); if (property.Name == PRIMARYKEY) { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); output.Append($" public {property.Type.CodeName} {property.Name} {{get;set;}} \r\n"); } else { switch (property.Type.CodeName) { - case "string" when property.Name.Equals("Name", StringComparison.OrdinalIgnoreCase): - output.Append($" public {property.Type.CodeName} {property.Name} {{get;set;}} = String.Empty; \r\n"); - break; - case "string" when !property.Name.Equals("Name", StringComparison.OrdinalIgnoreCase) && !property.Type.IsArray && !property.Type.IsDictionary: - output.Append($" public {property.Type.CodeName}? {property.Name} {{get;set;}} \r\n"); - break; - case "string" when !property.Name.Equals("Name", StringComparison.OrdinalIgnoreCase) && property.Type.IsArray: - output.Append($" public HashSet<{property.Type.CodeName}>? {property.Name} {{get;set;}} \r\n"); + case "string": + case "string?": + if (property.Type.IsArray) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName}[]{(property.Type.IsOptional ? "?" : "")} {property.Name} {{get;set;}} \r\n"); + } + else if (property.Type.IsDictionary) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public Dictionary<{property.Type.CodeName},{property.Type.CodeName}>{(property.Type.IsOptional ? "?" : "")} {property.Name} {{get;set;}} \r\n"); + } + else + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName}{(property.Name.Equals("Name") ? "" : "?")} {property.Name} {{get;set;}} \r\n"); + } break; + case "System.DateTime": case "System.DateTime?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); output.Append($" public DateTime? {property.Name} {{get;set;}} \r\n"); break; - case "System.DateTime": - output.Append($" public DateTime {property.Name} {{get;set;}} \r\n"); + case "System.TimeSpan": + case "System.TimeSpan?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public TimeSpan? {property.Name} {{get;set;}} \r\n"); + break; + case "System.DateTimeOffset": + case "System.DateTimeOffset?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public DateTimeOffset? {property.Name} {{get;set;}} \r\n"); + break; + case "System.Guid": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public Guid {property.Name} {{get;set;}} \r\n"); break; + case "System.Guid?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public Guid? {property.Name} {{get;set;}} \r\n"); + break; + case "bool?": + case "bool": + case "byte?": + case "byte": + case "char?": + case "char": + case "float?": + case "float": case "decimal?": case "decimal": case "int?": case "int": case "double?": case "double": - output.Append($" public {property.Type.CodeName} {property.Name} {{get;set;}} \r\n"); + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName}{(property.Type.IsArray ? "[]" : "")} {property.Name} {{get;set;}} \r\n"); break; default: - if (property.Type.CodeName.Any(x => x == '?')) + if (objectlist != null && objectlist.Any(x => x.FullName.Equals(property.Type.CodeName))) { - output.Append($" public {property.Type.CodeName} {property.Name} {{get;set;}} \r\n"); + var complexType = property.Type.CodeName.Split('.').Last(); + var relatedObject = objectlist.First(x => x.FullName.Equals(property.Type.CodeName)); + if (relatedObject.IsEnum) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {complexType}? {property.Name} {{get;set;}} \r\n"); + } + else + { + complexType = complexType + "Dto"; + if (property.Type.IsArray) + { + complexType = $"List<{complexType}>?"; + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {complexType} {property.Name} {{get;set;}} \r\n"); + } + else + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {complexType}? {property.Name} {{get;set;}} \r\n"); + } + } } else { - if (property.Type.IsOptional) + if (property.Name.Equals("Tenant")) { - output.Append($" public {property.Type.CodeName}? {property.Name} {{get;set;}} \r\n"); + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public TenantDto? {property.Name} {{get;set;}} \r\n"); } - else + } + break; + } + } + } + + + if (classObject.BaseName.Equals("OwnerPropertyEntity")) + { + output.Append($" [Description(\"Owner\")] public ApplicationUserDto? Owner {{ get; set; }} \r\n"); + output.Append($" [Description(\"Last modifier\")] public ApplicationUserDto? LastModifier {{ get; set; }} \r\n"); + } + return output.ToString(); + } + + private static string createDtoFieldDefinitionWithoutList(IntellisenseObject classObject, IEnumerable objectlist = null) + { + var output = new StringBuilder(); + foreach (var property in classObject.Properties.Where(x => x.Type.IsDictionary == false)) + { + if (property.Name == PRIMARYKEY) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName} {property.Name} {{get;set;}} \r\n"); + } + else + { + switch (property.Type.CodeName) + { + case "string": + case "string?": + if (property.Type.IsArray) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName}[]{(property.Type.IsOptional ? "?" : "")} {property.Name} {{get;set;}} \r\n"); + } + else if (property.Type.IsDictionary) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public Dictionary<{property.Type.CodeName},{property.Type.CodeName}>{(property.Type.IsOptional ? "?" : "")} {property.Name} {{get;set;}} \r\n"); + } + else + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName}{(property.Name.Equals("Name") ? "" : "?")} {property.Name} {{get;set;}} \r\n"); + } + break; + case "System.DateTime": + case "System.DateTime?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public DateTime? {property.Name} {{get;set;}} \r\n"); + break; + case "System.TimeSpan": + case "System.TimeSpan?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public TimeSpan? {property.Name} {{get;set;}} \r\n"); + break; + case "System.DateTimeOffset": + case "System.DateTimeOffset?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public DateTimeOffset? {property.Name} {{get;set;}} \r\n"); + break; + case "System.Guid": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public Guid {property.Name} {{get;set;}} \r\n"); + break; + case "System.Guid?": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public Guid? {property.Name} {{get;set;}} \r\n"); + break; + case "bool?": + case "bool": + case "byte?": + case "byte": + case "char?": + case "char": + case "float?": + case "float": + case "decimal?": + case "decimal": + case "int?": + case "int": + case "double?": + case "double": + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {property.Type.CodeName}{(property.Type.IsArray ? "[]" : "")} {property.Name} {{get;set;}} \r\n"); + break; + default: + if (objectlist != null && objectlist.Any(x => x.FullName.Equals(property.Type.CodeName))) + { + var complexType = property.Type.CodeName.Split('.').Last(); + var relatedObject = objectlist.First(x => x.FullName.Equals(property.Type.CodeName)); + if (relatedObject.IsEnum) + { + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {complexType}? {property.Name} {{get;set;}} \r\n"); + } + else if(!property.Type.IsArray) + { + complexType = complexType + "Dto?"; + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public {complexType} {property.Name} {{get;set;}} \r\n"); + } + } + else + { + if (property.Name.Equals("Tenant")) { - output.Append($" public {property.Type.CodeName} {property.Name} {{get;set;}} \r\n"); + output.Append($" [Description(\"{splitCamelCase(property.Name)}\")]\r\n"); + output.Append($" public TenantDto? {property.Name} {{get;set;}} \r\n"); } } break; } - } } + + + if (classObject.BaseName.Equals("OwnerPropertyEntity")) + { + output.Append($" [Description(\"Owner\")] public ApplicationUserDto? Owner {{ get; set; }} \r\n"); + output.Append($" [Description(\"Last modifier\")] public ApplicationUserDto? LastModifier {{ get; set; }} \r\n"); + } return output.ToString(); } - private static string createImportFuncExpression(IntellisenseObject classObject) + + private static string createImportFuncExpression(IntellisenseObject classObject, IEnumerable objectlist = null) { var output = new StringBuilder(); - foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true)) + foreach (var property in classObject.Properties.Where(x => !x.Type.IsDictionary && !x.Type.IsArray)) { if (property.Name == PRIMARYKEY) continue; + if (property.Type.CodeName.StartsWith("bool")) { - output.Append($"{{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} =Convert.ToBoolean(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]]) }}, \r\n"); + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} =Convert.ToBoolean(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]]) }}, \r\n"); + } + else if (property.Type.CodeName.StartsWith("System.DateTime")) + { + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} =DateTime.Parse(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]].ToString()) }}, \r\n"); + } + else if (property.Type.CodeName.StartsWith("System.TimeSpan")) + { + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} =TimeSpan.Parse(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]].ToString()) }}, \r\n"); + } + else if (property.Type.CodeName.StartsWith("int")) + { + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} =Convert.ToInt32(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]]) }}, \r\n"); + } + else if (property.Type.CodeName.StartsWith("decimal") || property.Type.CodeName.StartsWith("float")) + { + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} =Convert.ToDecimal(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]]) }}, \r\n"); } else { - output.Append($"{{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} = row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]].ToString() }}, \r\n"); + if (property.Type.IsKnownType) + { + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} = row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]].ToString() }}, \r\n"); + } + else if(objectlist != null) + { + var relatedObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName) && x.IsEnum); + if (relatedObject != null) + { + var enumType = property.Type.CodeName.Split('.').Last(); + output.Append($" {{ _localizer[_dto.GetMemberDescription(x=>x.{property.Name})], (row, item) => item.{property.Name} = Enum.Parse<{enumType}>(row[_localizer[_dto.GetMemberDescription(x=>x.{property.Name})]].ToString()) }}, \r\n"); + } + } + } } return output.ToString(); @@ -272,60 +512,104 @@ private static string createImportFuncExpression(IntellisenseObject classObject) private static string createTemplateFieldDefinition(IntellisenseObject classObject) { var output = new StringBuilder(); - foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true)) + foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true && !x.Type.IsDictionary && !x.Type.IsArray)) { if (property.Name == PRIMARYKEY) continue; output.Append($"_localizer[_dto.GetMemberDescription(x=>x.{property.Name})], \r\n"); } return output.ToString(); } - private static string createExportFuncExpression(IntellisenseObject classObject) + private static string createExportFuncExpression(IntellisenseObject classObject, IEnumerable objectlist = null) { var output = new StringBuilder(); - foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true)) + foreach (var property in classObject.Properties.Where(x => !x.Type.IsDictionary && !x.Type.IsArray)) { - output.Append($"{{_localizer[_dto.GetMemberDescription(x=>x.{property.Name})],item => item.{property.Name}}}, \r\n"); + if (property.Type.IsKnownType) + { + output.Append($" {{_localizer[_dto.GetMemberDescription(x=>x.{property.Name})],item => item.{property.Name}}}, \r\n"); + } + else if (property.Type.CodeName.StartsWith("System.DateTime")) + { + output.Append($" {{_localizer[_dto.GetMemberDescription(x=>x.{property.Name})],item => item.{property.Name}}}, \r\n"); + } + else if (property.Type.CodeName.StartsWith("System.TimeSpan")) + { + output.Append($" {{_localizer[_dto.GetMemberDescription(x=>x.{property.Name})],item => item.{property.Name}}}, \r\n"); + } + else if (objectlist != null) + { + var relatedObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName) && x.IsEnum); + if (relatedObject != null) + { + output.Append($" {{_localizer[_dto.GetMemberDescription(x=>x.{property.Name})],item => item.{property.Name}?.ToString()}}, \r\n"); + } + } + } return output.ToString(); } - private static string createMudTdHeaderDefinition(IntellisenseObject classObject) + private static string createMudTdHeaderDefinition(string name,IntellisenseObject classObject, IEnumerable objectlist = null) { var output = new StringBuilder(); var defaultfieldName = new string[] { "Name", "Description" }; if (classObject.Properties.Where(x => x.Type.IsKnownType == true && defaultfieldName.Contains(x.Name)).Any()) { - output.Append($" x.Name\" Title=\"@L[_currentDto.GetMemberDescription(x=>x.Name)]\"> \r\n"); + output.Append($" x.Name\" Title=\"@L[_{name.ToLower()}Dto.GetMemberDescription(x=>x.Name)]\"> \r\n"); output.Append(" \r\n"); output.Append($"
\r\n"); if (classObject.Properties.Where(x => x.Type.IsKnownType == true && x.Name == defaultfieldName.First()).Any()) { - output.Append($" @context.Item.Name\r\n"); + output.Append($" @context.Item.Name\r\n"); } if (classObject.Properties.Where(x => x.Type.IsKnownType == true && x.Name == defaultfieldName.Last()).Any()) { - output.Append($" @context.Item.Description\r\n"); + output.Append($" @context.Item.Description\r\n"); } output.Append($"
\r\n"); output.Append("
\r\n"); output.Append($"
\r\n"); } - foreach (var property in classObject.Properties.Where(x => !defaultfieldName.Contains(x.Name))) + foreach (var property in classObject.Properties.Where(x => !x.Type.IsDictionary && !x.Type.IsArray && !defaultfieldName.Contains(x.Name))) { if (property.Name == PRIMARYKEY) continue; - output.Append(" "); - output.Append($" x.{property.Name}\" Title=\"@L[_currentDto.GetMemberDescription(x=>x.{property.Name})]\" />\r\n"); + if (property.Type.IsKnownType) + { + output.Append(" "); + output.Append($" x.{property.Name}\" Title=\"@L[_{name.ToLower()}Dto.GetMemberDescription(x=>x.{property.Name})]\" />\r\n"); + } + else if (property.Type.CodeName.StartsWith("System.DateTime")) + { + output.Append($" x.{property.Name}\" Title=\"@L[_{name.ToLower()}Dto.GetMemberDescription(x=>x.{property.Name})]\" />\r\n"); + } + else if (property.Type.CodeName.StartsWith("System.TimeSpan")) + { + output.Append($" x.{property.Name}\" Title=\"@L[_{name.ToLower()}Dto.GetMemberDescription(x=>x.{property.Name})]\" />\r\n"); + } + else if (objectlist != null) + { + var relatedObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName) && x.IsEnum); + if (relatedObject != null) + { + output.Append(" "); + output.Append($" x.{property.Name}\" Title=\"@L[_{name.ToLower()}Dto.GetMemberDescription(x=>x.{property.Name})]\">\r\n"); + output.Append(" \r\n"); + output.Append($" \r\n"); + output.Append(" \r\n"); + output.Append($"\r\n"); + } + } } return output.ToString(); } - private static string createMudTdDefinition(IntellisenseObject classObject) + private static string createMudTdDefinition(string name,IntellisenseObject classObject) { var output = new StringBuilder(); var defaultfieldName = new string[] { "Name", "Description" }; if (classObject.Properties.Where(x => x.Type.IsKnownType == true && defaultfieldName.Contains(x.Name)).Any()) { - output.Append($"x.Name)]\"> \r\n"); + output.Append($"x.Name)]\"> \r\n"); output.Append(" "); output.Append($"
\r\n"); if (classObject.Properties.Where(x => x.Type.IsKnownType == true && x.Name == defaultfieldName.First()).Any()) @@ -333,44 +617,45 @@ private static string createMudTdDefinition(IntellisenseObject classObject) output.Append(" "); output.Append($" @context.Name\r\n"); } - if (classObject.Properties.Where(x => x.Type.IsKnownType == true && x.Name == defaultfieldName.Last()).Any()) { + if (classObject.Properties.Where(x => x.Type.IsKnownType == true && x.Name == defaultfieldName.Last()).Any()) + { output.Append(" "); - output.Append($" @context.Description\r\n"); - } + output.Append($" @context.Description\r\n"); + } output.Append(" "); output.Append($"
\r\n"); output.Append(" "); output.Append($"
\r\n"); } - foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true && !defaultfieldName.Contains(x.Name))) + foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true && !x.Type.IsDictionary && !x.Type.IsArray && !defaultfieldName.Contains(x.Name))) { if (property.Name == PRIMARYKEY) continue; output.Append(" "); if (property.Type.CodeName.StartsWith("bool", StringComparison.OrdinalIgnoreCase)) { - output.Append($" x.{property.Name})]\" > \r\n"); + output.Append($" x.{property.Name})]\" > \r\n"); } - else if(property.Type.CodeName.Equals("System.DateTime", StringComparison.OrdinalIgnoreCase)) + else if (property.Type.CodeName.Equals("System.DateTime", StringComparison.OrdinalIgnoreCase)) { - output.Append($" x.{property.Name}))]\" >@context.{property.Name}.Date.ToString(\"d\") \r\n"); + output.Append($" x.{property.Name}))]\" >@context.{property.Name}.Date.ToString(\"d\") \r\n"); } else if (property.Type.CodeName.Equals("System.DateTime?", StringComparison.OrdinalIgnoreCase)) { - output.Append($" x{property.Name})]\" >@context.{property.Name}?.Date.ToString(\"d\") \r\n"); + output.Append($" x{property.Name})]\" >@context.{property.Name}?.Date.ToString(\"d\") \r\n"); } else { - output.Append($" .{property.Name})]\" >@context.{property.Name} \r\n"); + output.Append($" .{property.Name})]\" >@context.{property.Name} \r\n"); } - + } return output.ToString(); } - private static string createMudFormFieldDefinition(IntellisenseObject classObject) + private static string createMudFormFieldDefinition(IntellisenseObject classObject, IEnumerable objectlist = null) { var output = new StringBuilder(); - foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true)) + foreach (var property in classObject.Properties.Where(x => !x.Type.IsDictionary && !x.Type.IsArray)) { if (property.Name == PRIMARYKEY) continue; switch (property.Type.CodeName.ToLower()) @@ -378,14 +663,14 @@ private static string createMudFormFieldDefinition(IntellisenseObject classObjec case "string" when property.Name.Equals("Name", StringComparison.OrdinalIgnoreCase): output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Value=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" Required=\"true\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append($" x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Required=\"true\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; case "string" when property.Name.Equals("Description", StringComparison.OrdinalIgnoreCase): output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" Lines=\"3\" For=\"@(() => model.{property.Name})\" @bind-Value=\"model.{property.Name}\">\r\n"); + output.Append($" x.{property.Name})]\" For=\"@(() => _model.{property.Name})\" @bind-Value=\"_model.{property.Name}\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; @@ -393,7 +678,7 @@ private static string createMudFormFieldDefinition(IntellisenseObject classObjec case "bool": output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Checked=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" >\r\n"); + output.Append($" x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" >\r\n"); output.Append(" "); output.Append($" \r\n"); break; @@ -401,7 +686,7 @@ private static string createMudFormFieldDefinition(IntellisenseObject classObjec case "int": output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Value=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" Min=\"0\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append($" x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Min=\"0\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; @@ -409,7 +694,7 @@ private static string createMudFormFieldDefinition(IntellisenseObject classObjec case "decimal": output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Value=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" Min=\"0.00m\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append($" x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Min=\"0.00m\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; @@ -417,40 +702,261 @@ private static string createMudFormFieldDefinition(IntellisenseObject classObjec case "double": output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Value=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" Min=\"0.00\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append($" x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Min=\"0.00\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; - case "system.datetime?": + case "system.datetime": + case "system.datetime?": output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Date=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append($" x.{property.Name})]\" @bind-Date=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; - default: + case "system.timespan": + case "system.timespan?": output.Append($" \r\n"); output.Append(" "); - output.Append($" x.{property.Name})]\" @bind-Value=\"model.{property.Name}\" For=\"@(() => model.{property.Name})\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append($" x.{property.Name})]\" @bind-Time=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); output.Append(" "); output.Append($" \r\n"); break; + default: + if (property.Type.IsKnownType) + { + output.Append($" \r\n"); + output.Append(" "); + output.Append($" x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + } + else if (objectlist != null) + { + var relatedObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName) && x.IsEnum); + if (relatedObject != null) + { + var enumType = property.Type.CodeName.Split('.').Last(); + output.Append($" \r\n"); + output.Append(" "); + output.Append($" \" Label=\"@L[_model.GetMemberDescription(x=>x.{property.Name})]\" @bind-Value=\"_model.{property.Name}\" For=\"@(() => _model.{property.Name})\" Required=\"false\" RequiredError=\"@L[\"{splitCamelCase(property.Name).ToLower()} is required!\"]\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + } + } + break; } - + } return output.ToString(); } + private static string createReadyonlyFieldDefinition(IntellisenseObject classObject, IEnumerable objectlist = null) + { + var output = new StringBuilder(); + foreach (var property in classObject.Properties.Where(x => !x.Type.IsDictionary && !x.Type.IsArray)) + { + if (property.Name == PRIMARYKEY) continue; + switch (property.Type.CodeName.ToLower()) + { + + case "bool?": + case "bool": + output.Append($" \r\n"); + output.Append(" "); + output.Append($" x.{property.Name})]\" Value=\"_model.{property.Name}\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + break; + case "system.datetime": + case "system.datetime?": + output.Append($" \r\n"); + output.Append(" "); + output.Append($" x.{property.Name})]\" Value=\"@_model.{property.Name}?.ToString(\"d\")\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + break; + case "system.timespan": + case "system.timespan?": + output.Append($" \r\n"); + output.Append(" "); + output.Append($" x.{property.Name})]\" Value=\"@_model.{property.Name}?.ToString(\"hh:mm\")\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + break; + default: + if (property.Type.IsKnownType) + { + output.Append($" \r\n"); + output.Append(" "); + output.Append($" x.{property.Name})]\" Value=\"_model.{property.Name}\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + } + else if(objectlist!=null) + { + var relatedObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName) && x.IsEnum); + if (relatedObject != null) + { + var enumType = property.Type.CodeName.Split('.').Last(); + output.Append($" \r\n"); + output.Append(" "); + output.Append($" x.{property.Name})]\" Value=\"_model.{property.Name}?.GetDescription()\">\r\n"); + output.Append(" "); + output.Append($" \r\n"); + } + } + break; + + } + + } + return output.ToString(); + } - private static string createFieldAssignmentDefinition(IntellisenseObject classObject) + private static string createFieldAssignmentDefinition(IntellisenseObject classObject, IEnumerable objectlist = null) { var output = new StringBuilder(); - foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true)) + foreach (var property in classObject.Properties.Where(x => x.Type.IsKnownType == true && x.Name != "Id")) { - output.Append($" "); output.Append($" {property.Name} = dto.{property.Name}, \r\n"); } + foreach(var property in classObject.Properties.Where(x => x.Type.IsKnownType == false && x.Name != "Id")) + { + if (property.Type.CodeName.StartsWith("System.")) + { + output.Append($" {property.Name} = dto.{property.Name}, \r\n"); + } + else if (objectlist != null) + { + var relatedObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName) && x.IsEnum); + if (relatedObject != null) + { + output.Append($" {property.Name} = dto.{property.Name}, \r\n"); + } + } + } + return output.ToString(); + } + + private static string createEntityTypeBuilderConfirmation(IntellisenseObject classObject, IEnumerable objectlist = null) + { + var output = new StringBuilder(); + foreach (var property in classObject.Properties.Where(x => x.Name != "Id")) + { + switch (property.Type.CodeName) + { + case "string": + case "string?": + if (property.Type.IsArray) + { + output.Append($" builder.Property(e => e.{property.Name}).HasStringListConversion(); \r\n"); + } + else if (property.Type.IsDictionary) + { + output.Append($" builder.Property(u => u.{property.Name}).HasJsonConversion(); \r\n"); + } + else if (property.Name.Equals("Name")) + { + output.Append($" builder.HasIndex(x => x.{property.Name}); \r\n"); + output.Append($" builder.Property(x => x.{property.Name}).HasMaxLength(50).IsRequired(); \r\n"); + } + else + { + output.Append($" builder.Property(x => x.{property.Name}).HasMaxLength(255); \r\n"); + } + break; + default: + if (!property.Type.IsKnownType && objectlist!=null) + { + var refObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName)); + if (refObject != null) + { + var complexType = property.Type.CodeName.Split('.').Last(); + if (refObject.IsEnum) + { + output.Append($" builder.HasIndex(x => x.{property.Name}); \r\n"); + output.Append($" builder.Property(t => t.{property.Name}).HasConversion().HasMaxLength(50); \r\n"); + } + else if (!property.Type.IsArray) + { + var foreignKey = property.Name + "Id"; + if (classObject.Properties.Any(x => x.Name.Equals(foreignKey))) + { + output.Append($" builder.HasOne(x => x.{property.Name}).WithMany().HasForeignKey(x => x.{foreignKey}); \r\n"); + } + } + } + else + { + if (property.Name.Equals("Tenant") && classObject.Properties.Any(x => x.Name.Equals("TenantId"))) + { + output.Append($" builder.HasOne(x => x.{property.Name}).WithMany().HasForeignKey(x => x.TenantId); \r\n"); + output.Append($" builder.Navigation(e => e.{property.Name}).AutoInclude(); \r\n"); + } + } + + } + break; + } + } + + if (classObject.BaseName.Equals("OwnerPropertyEntity")) + { + output.Append($" builder.HasOne(x => x.Owner).WithMany().HasForeignKey(x => x.CreatedBy); \r\n"); + output.Append($" builder.HasOne(x => x.LastModifier).WithMany().HasForeignKey(x => x.LastModifiedBy); \r\n"); + output.Append($" builder.Navigation(e => e.Owner).AutoInclude(); \r\n"); + output.Append($" builder.Navigation(e => e.LastModifier).AutoInclude(); \r\n"); + } + return output.ToString(); + } + + + private static string createComandValidatorRuleFor(IntellisenseObject classObject, IEnumerable objectlist = null) + { + var output = new StringBuilder(); + foreach (var property in classObject.Properties.Where(x => x.Name != "Id")) + { + switch (property.Type.CodeName) + { + case "string": + case "string?": + if (property.Name.Equals("Name")) + { + output.Append($" RuleFor(v => v.{property.Name}).MaximumLength(50).NotEmpty(); \r\n"); + } + else if (!property.Type.IsDictionary && !property.Type.IsArray) + { + output.Append($" RuleFor(v => v.{property.Name}).MaximumLength(255); \r\n"); + } + break; + case "System.TimeSpan": + case "System.DateTime": + case "System.DateTimeOffset": + case "int": + case "decimal": + case "float": + output.Append($" RuleFor(v => v.{property.Name}).NotNull(); \r\n"); + break; + default: + if (!property.Type.IsKnownType && objectlist!=null) + { + var refObject = objectlist.FirstOrDefault(x => x.FullName.Equals(property.Type.CodeName)); + if (refObject != null) + { + var complexType = property.Type.CodeName.Split('.').Last(); + if (refObject.IsEnum) + { + output.Append($" RuleFor(v => v.{property.Name}).NotNull(); \r\n"); + } + + } + } + break; + } + + } return output.ToString(); } } diff --git a/src/Templates/Caching/.cachekey.cs.txt b/src/Templates/Caching/.cachekey.cs.txt index 01fa54f..0650fb0 100644 --- a/src/Templates/Caching/.cachekey.cs.txt +++ b/src/Templates/Caching/.cachekey.cs.txt @@ -1,35 +1,46 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines static methods and properties for managing cache keys and expiration +// settings for {itemname}-related data. This includes creating unique cache keys for +// various {itemnamelowercase} queries (such as getting all {itemnamelowercase}s, {itemnamelowercase}s by ID, etc.), +// managing the cache expiration tokens to control cache validity, and providing a +// mechanism to refresh cached data in a thread-safe manner. +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings namespace {namespace}; - +/// +/// Static class for managing cache keys and expiration for {itemname}-related data. +/// public static class {itemname}CacheKey { - private static readonly TimeSpan refreshInterval = TimeSpan.FromHours(3); public const string GetAllCacheKey = "all-{nameofPlural}"; public static string GetPaginationCacheKey(string parameters) { return $"{itemname}CacheKey:{nameofPlural}WithPaginationQuery,{parameters}"; } + public static string GetExportCacheKey(string parameters) { + return $"{itemname}CacheKey:ExportCacheKey,{parameters}"; + } public static string GetByNameCacheKey(string parameters) { return $"{itemname}CacheKey:GetByNameCacheKey,{parameters}"; } public static string GetByIdCacheKey(string parameters) { return $"{itemname}CacheKey:GetByIdCacheKey,{parameters}"; } - static {itemname}CacheKey() - { - _tokensource = new CancellationTokenSource(refreshInterval); - } - private static CancellationTokenSource _tokensource; - public static CancellationTokenSource SharedExpiryTokenSource() + public static IEnumerable? Tags => new string[] { "{itemnamelowercase}" }; + public static void Refresh() { - if (_tokensource.IsCancellationRequested) - { - _tokensource = new CancellationTokenSource(refreshInterval); - } - return _tokensource; + FusionCacheFactory.RemoveByTags(Tags); } - public static void Refresh() => SharedExpiryTokenSource().Cancel(); - public static MemoryCacheEntryOptions MemoryCacheEntryOptions => new MemoryCacheEntryOptions().AddExpirationToken(new CancellationChangeToken(SharedExpiryTokenSource().Token)); } diff --git a/src/Templates/Commands/AddEdit/.cs.txt b/src/Templates/Commands/AddEdit/.cs.txt index f3d4e22..b00ad79 100644 --- a/src/Templates/Commands/AddEdit/.cs.txt +++ b/src/Templates/Commands/AddEdit/.cs.txt @@ -1,66 +1,75 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Command for adding/editing a {itemnamelowercase} entity with validation, mapping, +// domain events, and cache invalidation. +// Documentation: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings + -using {selectns}.{nameofPlural}.DTOs; using {selectns}.{nameofPlural}.Caching; +using {selectns}.{nameofPlural}.DTOs; namespace {namespace}; public class AddEdit{itemname}Command: ICacheInvalidatorRequest> { [Description("Id")] public int Id { get; set; } - {dtoFieldDefinition} + {dtoFieldDefinitionWithoutList} public string CacheKey => {itemname}CacheKey.GetAllCacheKey; - public CancellationTokenSource? SharedExpiryTokenSource => {itemname}CacheKey.SharedExpiryTokenSource(); - + public IEnumerable? Tags => {itemname}CacheKey.Tags; private class Mapping : Profile { public Mapping() { - CreateMap<{itemname}Dto,AddEdit{itemname}Command>(MemberList.None); - CreateMap(MemberList.None); - + CreateMap<{itemname}Dto, AddEdit{itemname}Command>(MemberList.None); + CreateMap(MemberList.None); } } } - public class AddEdit{itemname}CommandHandler : IRequestHandler> +public class AddEdit{itemname}CommandHandler : IRequestHandler> +{ + private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; + public AddEdit{itemname}CommandHandler( + IMapper mapper, + IApplicationDbContext context) { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - private readonly IStringLocalizer _localizer; - public AddEdit{itemname}CommandHandler( - IApplicationDbContext context, - IStringLocalizer localizer, - IMapper mapper - ) - { - _context = context; - _localizer = localizer; - _mapper = mapper; - } - public async Task> Handle(AddEdit{itemname}Command request, CancellationToken cancellationToken) + _mapper = mapper; + _context = context; + } + public async Task> Handle(AddEdit{itemname}Command request, CancellationToken cancellationToken) + { + if (request.Id > 0) { - if (request.Id > 0) - { - var item = await _context.{nameofPlural}.FindAsync(new object[] { request.Id }, cancellationToken) ?? throw new NotFoundException($"{itemname} with id: [{request.Id}] not found."); - item = _mapper.Map(request, item); - // raise a update domain event - item.AddDomainEvent(new {itemname}UpdatedEvent(item)); - await _context.SaveChangesAsync(cancellationToken); - return await Result.SuccessAsync(item.Id); - } - else + var item = await _context.{nameofPlural}.FindAsync(request.Id, cancellationToken); + if (item == null) { - var item = _mapper.Map<{itemname}>(request); - // raise a create domain event - item.AddDomainEvent(new {itemname}CreatedEvent(item)); - _context.{nameofPlural}.Add(item); - await _context.SaveChangesAsync(cancellationToken); - return await Result.SuccessAsync(item.Id); + return await Result.FailureAsync($"{itemname} with id: [{request.Id}] not found."); } - + item = _mapper.Map(request, item); + // raise a update domain event + item.AddDomainEvent(new {itemname}UpdatedEvent(item)); + await _context.SaveChangesAsync(cancellationToken); + return await Result.SuccessAsync(item.Id); } + else + { + var item = _mapper.Map<{itemname}>(request); + // raise a create domain event + item.AddDomainEvent(new {itemname}CreatedEvent(item)); + _context.{nameofPlural}.Add(item); + await _context.SaveChangesAsync(cancellationToken); + return await Result.SuccessAsync(item.Id); + } + } +} diff --git a/src/Templates/Commands/AddEdit/.validator.cs.txt b/src/Templates/Commands/AddEdit/.validator.cs.txt index d4e8126..91b6645 100644 --- a/src/Templates/Commands/AddEdit/.validator.cs.txt +++ b/src/Templates/Commands/AddEdit/.validator.cs.txt @@ -1,5 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Validator for AddEdit{itemname}Command: enforces field length and required property rules for {itemname} entities. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings + namespace {namespace}; @@ -7,10 +17,7 @@ public class AddEdit{itemname}CommandValidator : AbstractValidator v.Name) - .MaximumLength(256) - .NotEmpty(); - + {commandValidatorRuleFor} } } diff --git a/src/Templates/Commands/Create/.cs.txt b/src/Templates/Commands/Create/.cs.txt index 22e7756..38e86fb 100644 --- a/src/Templates/Commands/Create/.cs.txt +++ b/src/Templates/Commands/Create/.cs.txt @@ -1,7 +1,16 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; -using {selectns}.{nameofPlural}.DTOs; +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Command and handler for creating a new {itemname}. +// Uses caching invalidation and domain events for data consistency. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings + using {selectns}.{nameofPlural}.Caching; namespace {namespace}; @@ -10,33 +19,28 @@ public class Create{itemname}Command: ICacheInvalidatorRequest> { [Description("Id")] public int Id { get; set; } - {dtoFieldDefinition} + {dtoFieldDefinitionWithoutList} public string CacheKey => {itemname}CacheKey.GetAllCacheKey; - public CancellationTokenSource? SharedExpiryTokenSource => {itemname}CacheKey.SharedExpiryTokenSource(); - private class Mapping : Profile + public IEnumerable? Tags => {itemname}CacheKey.Tags; + private class Mapping : Profile { public Mapping() { - CreateMap<{itemname}Dto,Create{itemname}Command>(MemberList.None); - CreateMap(MemberList.None); + CreateMap(MemberList.None); } } } public class Create{itemname}CommandHandler : IRequestHandler> { - private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly IStringLocalizer _localizer; + private readonly IApplicationDbContext _context; public Create{itemname}CommandHandler( - IApplicationDbContext context, - IStringLocalizer localizer, - IMapper mapper - ) + IMapper mapper, + IApplicationDbContext context) { - _context = context; - _localizer = localizer; _mapper = mapper; + _context = context; } public async Task> Handle(Create{itemname}Command request, CancellationToken cancellationToken) { diff --git a/src/Templates/Commands/Create/.validator.cs.txt b/src/Templates/Commands/Create/.validator.cs.txt index 89709c3..04e158e 100644 --- a/src/Templates/Commands/Create/.validator.cs.txt +++ b/src/Templates/Commands/Create/.validator.cs.txt @@ -1,5 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Validator for Create{itemname}Command: enforces max lengths and required fields for {itemname} entities. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings namespace {namespace}; @@ -7,11 +16,7 @@ public class Create{itemname}CommandValidator : AbstractValidator v.Name) - .MaximumLength(256) - .NotEmpty(); - + {commandValidatorRuleFor} } } diff --git a/src/Templates/Commands/Delete/.cs.txt b/src/Templates/Commands/Delete/.cs.txt index a2df2d8..2747dec 100644 --- a/src/Templates/Commands/Delete/.cs.txt +++ b/src/Templates/Commands/Delete/.cs.txt @@ -1,51 +1,53 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Command and handler for deleting {itemname} entities. +// Implements cache invalidation and triggers domain events. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings using {selectns}.{nameofPlural}.Caching; - namespace {namespace}; - public class Delete{itemname}Command: ICacheInvalidatorRequest> +public class Delete{itemname}Command: ICacheInvalidatorRequest +{ + public int[] Id { get; } + public string CacheKey => {itemname}CacheKey.GetAllCacheKey; + public IEnumerable? Tags => {itemname}CacheKey.Tags; + public Delete{itemname}Command(int[] id) + { + Id = id; + } +} + +public class Delete{itemname}CommandHandler : + IRequestHandler + +{ + private readonly IApplicationDbContext _context; + public Delete{itemname}CommandHandler( + IApplicationDbContext context) { - public int[] Id { get; } - public string CacheKey => {itemname}CacheKey.GetAllCacheKey; - public CancellationTokenSource? SharedExpiryTokenSource => {itemname}CacheKey.SharedExpiryTokenSource(); - public Delete{itemname}Command(int[] id) - { - Id = id; - } + _context = context; } - - public class Delete{itemname}CommandHandler : - IRequestHandler> - + public async Task Handle(Delete{itemname}Command request, CancellationToken cancellationToken) { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - private readonly IStringLocalizer _localizer; - public Delete{itemname}CommandHandler( - IApplicationDbContext context, - IStringLocalizer localizer, - IMapper mapper - ) + var items = await _context.{nameofPlural}.Where(x=>request.Id.Contains(x.Id)).ToListAsync(cancellationToken); + foreach (var item in items) { - _context = context; - _localizer = localizer; - _mapper = mapper; + // raise a delete domain event + item.AddDomainEvent(new {itemname}DeletedEvent(item)); + _context.{nameofPlural}.Remove(item); } - public async Task> Handle(Delete{itemname}Command request, CancellationToken cancellationToken) - { - var items = await _context.{nameofPlural}.Where(x=>request.Id.Contains(x.Id)).ToListAsync(cancellationToken); - foreach (var item in items) - { - // raise a delete domain event - item.AddDomainEvent(new {itemname}DeletedEvent(item)); - _context.{nameofPlural}.Remove(item); - } - var result = await _context.SaveChangesAsync(cancellationToken); - return await Result.SuccessAsync(result); - } - + await _context.SaveChangesAsync(cancellationToken); + return await Result.SuccessAsync(); } +} + diff --git a/src/Templates/Commands/Delete/.validator.cs.txt b/src/Templates/Commands/Delete/.validator.cs.txt index feb3c60..fb37591 100644 --- a/src/Templates/Commands/Delete/.validator.cs.txt +++ b/src/Templates/Commands/Delete/.validator.cs.txt @@ -1,5 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Validator for Delete{itemname}Command: ensures the ID list for {itemnamelowercase} is not null and contains only positive IDs. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings namespace {namespace}; diff --git a/src/Templates/Commands/Import/.cs.txt b/src/Templates/Commands/Import/.cs.txt index f706d20..7d34d1d 100644 --- a/src/Templates/Commands/Import/.cs.txt +++ b/src/Templates/Commands/Import/.cs.txt @@ -1,5 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Import command & template for {itemnamelowercase}s. +// Validates Excel data, prevents duplicates, and provides a template for bulk entry. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings using {selectns}.{nameofPlural}.DTOs; using {selectns}.{nameofPlural}.Caching; @@ -11,7 +21,7 @@ namespace {namespace}; public string FileName { get; set; } public byte[] Data { get; set; } public string CacheKey => {itemname}CacheKey.GetAllCacheKey; - public CancellationTokenSource? SharedExpiryTokenSource => {itemname}CacheKey.SharedExpiryTokenSource(); + public IEnumerable? Tags => {itemname}CacheKey.Tags; public Import{nameofPlural}Command(string fileName,byte[] data) { FileName = fileName; @@ -28,17 +38,15 @@ namespace {namespace}; IRequestHandler> { private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; private readonly IStringLocalizer _localizer; private readonly IExcelService _excelService; private readonly {itemname}Dto _dto = new(); - + private readonly IMapper _mapper; public Import{nameofPlural}CommandHandler( IApplicationDbContext context, + IMapper mapper, IExcelService excelService, - IStringLocalizer localizer, - IMapper mapper - ) + IStringLocalizer localizer) { _context = context; _localizer = localizer; @@ -76,9 +84,7 @@ namespace {namespace}; } public async Task> Handle(Create{nameofPlural}TemplateCommand request, CancellationToken cancellationToken) { - // TODO: Implement Import{nameofPlural}CommandHandler method var fields = new string[] { - // TODO: Define the fields that should be generate in the template, for example: {templateFieldDefinition} }; var result = await _excelService.CreateTemplateAsync(fields, _localizer[_dto.GetClassDescription()]); diff --git a/src/Templates/Commands/Import/.validator.cs.txt b/src/Templates/Commands/Import/.validator.cs.txt index 0070736..a5382de 100644 --- a/src/Templates/Commands/Import/.validator.cs.txt +++ b/src/Templates/Commands/Import/.validator.cs.txt @@ -1,5 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Validator for Import{itemname}sCommand: ensures the Data property is non-null and non-empty for {itemnamelowercase} import. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings namespace {namespace}; diff --git a/src/Templates/Commands/Update/.cs.txt b/src/Templates/Commands/Update/.cs.txt index 9da1994..ddbd1bd 100644 --- a/src/Templates/Commands/Update/.cs.txt +++ b/src/Templates/Commands/Update/.cs.txt @@ -1,6 +1,15 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -using System.ComponentModel; +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Update{itemname}Command & handler: updates an existing {itemname} with cache invalidation and raises {itemname}UpdatedEvent. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings + using {selectns}.{nameofPlural}.DTOs; using {selectns}.{nameofPlural}.Caching; @@ -10,43 +19,45 @@ public class Update{itemname}Command: ICacheInvalidatorRequest> { [Description("Id")] public int Id { get; set; } - {dtoFieldDefinition} - public string CacheKey => {itemname}CacheKey.GetAllCacheKey; - public CancellationTokenSource? SharedExpiryTokenSource => {itemname}CacheKey.SharedExpiryTokenSource(); + {dtoFieldDefinitionWithoutList} + public string CacheKey => {itemname}CacheKey.GetAllCacheKey; + public IEnumerable? Tags => {itemname}CacheKey.Tags; + private class Mapping : Profile { public Mapping() { + CreateMap(MemberList.None); CreateMap<{itemname}Dto,Update{itemname}Command>(MemberList.None); - CreateMap(MemberList.None); } } + } - public class Update{itemname}CommandHandler : IRequestHandler> +public class Update{itemname}CommandHandler : IRequestHandler> +{ + private readonly IApplicationDbContext _context; + private readonly IMapper _mapper; + public Update{itemname}CommandHandler( + IMapper mapper, + IApplicationDbContext context) + { + _context = context; + _mapper = mapper; + } + public async Task> Handle(Update{itemname}Command request, CancellationToken cancellationToken) { - private readonly IApplicationDbContext _context; - private readonly IMapper _mapper; - private readonly IStringLocalizer _localizer; - public Update{itemname}CommandHandler( - IApplicationDbContext context, - IStringLocalizer localizer, - IMapper mapper - ) - { - _context = context; - _localizer = localizer; - _mapper = mapper; - } - public async Task> Handle(Update{itemname}Command request, CancellationToken cancellationToken) - { - var item =await _context.{nameofPlural}.FindAsync( new object[] { request.Id }, cancellationToken)?? throw new NotFoundException($"{itemname} with id: [{request.Id}] not found."); - item = _mapper.Map(request, item); - // raise a update domain event - item.AddDomainEvent(new {itemname}UpdatedEvent(item)); - await _context.SaveChangesAsync(cancellationToken); - return await Result.SuccessAsync(item.Id); - } + var item = await _context.{nameofPlural}.FindAsync(request.Id, cancellationToken); + if (item == null) + { + return await Result.FailureAsync($"{itemname} with id: [{request.Id}] not found."); + } + item = _mapper.Map(request, item); + // raise a update domain event + item.AddDomainEvent(new {itemname}UpdatedEvent(item)); + await _context.SaveChangesAsync(cancellationToken); + return await Result.SuccessAsync(item.Id); } +} diff --git a/src/Templates/Commands/Update/.validator.cs.txt b/src/Templates/Commands/Update/.validator.cs.txt index da69274..37d41a2 100644 --- a/src/Templates/Commands/Update/.validator.cs.txt +++ b/src/Templates/Commands/Update/.validator.cs.txt @@ -1,5 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Validator for Update{itemname}Command: ensures required fields (e.g., Id, non-empty Name, max length for properties) are valid for updating a {itemnamelowercase}. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings namespace {namespace}; @@ -8,7 +17,7 @@ public class Update{itemname}CommandValidator : AbstractValidator v.Id).NotNull(); - RuleFor(v => v.Name).MaximumLength(256).NotEmpty(); + {commandValidatorRuleFor} } diff --git a/src/Templates/DTOs/.dto.cs.txt b/src/Templates/DTOs/.dto.cs.txt index 5c1c727..207e60b 100644 --- a/src/Templates/DTOs/.dto.cs.txt +++ b/src/Templates/DTOs/.dto.cs.txt @@ -1,7 +1,16 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// {itemname}Dto: transfers {itemnamelowercase} data between layers. +// Docs: https://docs.cleanarchitectureblazor.com/features/{itemnamelowercase} +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings + -using System.ComponentModel; namespace {namespace}; [Description("{nameofPlural}")] @@ -15,7 +24,13 @@ public class {itemname}Dto { public Mapping() { - CreateMap<{itemname}, {itemname}Dto>().ReverseMap(); + CreateMap<{itemname}, {itemname}Dto>(MemberList.None); + CreateMap<{itemname}Dto, {itemname}>(MemberList.None) + .ForMember(dest => dest.Created, opt => opt.Ignore()) + .ForMember(dest => dest.CreatedBy, opt => opt.Ignore()) + .ForMember(dest => dest.LastModified, opt => opt.Ignore()) + .ForMember(dest => dest.LastModifiedBy, opt => opt.Ignore()) + .ForMember(dest => dest.DomainEvents, opt => opt.Ignore()); } } } diff --git a/src/Templates/EventHandlers/.created.cs.txt b/src/Templates/EventHandlers/.created.cs.txt index 8c2b1e3..6c613ab 100644 --- a/src/Templates/EventHandlers/.created.cs.txt +++ b/src/Templates/EventHandlers/.created.cs.txt @@ -1,5 +1,13 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Handles {itemname}CreatedEvent: triggered when a new {itemnamelowercase} is created. +// Extendable for additional actions (e.g., notifications, system updates). +// +//------------------------------------------------------------------------------ + namespace {namespace}; @@ -15,7 +23,7 @@ public class {itemname}CreatedEventHandler : INotificationHandler<{itemname}Crea } public Task Handle({itemname}CreatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().FullName); + _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); return Task.CompletedTask; } } diff --git a/src/Templates/EventHandlers/.deleted.cs.txt b/src/Templates/EventHandlers/.deleted.cs.txt index 7744a16..c8a8975 100644 --- a/src/Templates/EventHandlers/.deleted.cs.txt +++ b/src/Templates/EventHandlers/.deleted.cs.txt @@ -1,5 +1,13 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Handles {itemname}DeletedEvent: triggered when a {itemnamelowercase} is deleted. +// Extendable for additional actions (e.g., notifications, system updates). +// +//------------------------------------------------------------------------------ + namespace {namespace}; @@ -15,7 +23,7 @@ namespace {namespace}; } public Task Handle({itemname}DeletedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().FullName); + _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); return Task.CompletedTask; } } diff --git a/src/Templates/EventHandlers/.updated.cs.txt b/src/Templates/EventHandlers/.updated.cs.txt index ea22262..eaf9b02 100644 --- a/src/Templates/EventHandlers/.updated.cs.txt +++ b/src/Templates/EventHandlers/.updated.cs.txt @@ -1,5 +1,13 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Handles {itemname}UpdatedEvent: triggered when a {itemnamelowercase} is updated. +// Extendable for additional actions (e.g., notifications, system updates). +// +//------------------------------------------------------------------------------ + namespace {namespace}; @@ -15,7 +23,7 @@ namespace {namespace}; } public Task Handle({itemname}UpdatedEvent notification, CancellationToken cancellationToken) { - _logger.LogInformation("Domain Event: {DomainEvent}", notification.GetType().FullName); + _logger.LogInformation("Handled domain event '{EventType}' with notification: {@Notification} ", notification.GetType().Name, notification); return Task.CompletedTask; } } diff --git a/src/Templates/Events/.createdevent.cs.txt b/src/Templates/Events/.createdevent.cs.txt index 7e8b560..62b5316 100644 --- a/src/Templates/Events/.createdevent.cs.txt +++ b/src/Templates/Events/.createdevent.cs.txt @@ -1,5 +1,12 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Domain event for when a new {itemnamelowercase} is created. +// +//------------------------------------------------------------------------------ + namespace {namespace}; diff --git a/src/Templates/Events/.deletedevent.cs.txt b/src/Templates/Events/.deletedevent.cs.txt index 64840a6..3525d34 100644 --- a/src/Templates/Events/.deletedevent.cs.txt +++ b/src/Templates/Events/.deletedevent.cs.txt @@ -1,5 +1,12 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Domain event for when a {itemnamelowercase} is deleted. +// +//------------------------------------------------------------------------------ + namespace {namespace}; diff --git a/src/Templates/Events/.updatedevent.cs.txt b/src/Templates/Events/.updatedevent.cs.txt index 6a21c61..05fafdb 100644 --- a/src/Templates/Events/.updatedevent.cs.txt +++ b/src/Templates/Events/.updatedevent.cs.txt @@ -1,5 +1,12 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Domain event for when a {itemnamelowercase} is updated. +// +//------------------------------------------------------------------------------ + namespace {namespace}; diff --git a/src/Templates/Pages/.create.razor.txt b/src/Templates/Pages/.create.razor.txt new file mode 100644 index 0000000..a921cea --- /dev/null +++ b/src/Templates/Pages/.create.razor.txt @@ -0,0 +1,74 @@ +@page "/pages/{nameofplurallowercase}/create" +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Commands.Create + +@inherits MudComponentBase +@inject IValidationService Validator +@inject IStringLocalizer<{nameofPlural}> L +@attribute [Authorize(Policy = Permissions.{nameofPlural}.Create)] + +@Title + + + + + + @Title + + + + + + {mudFormFieldDefinition} + + + + + @ConstantString.Save + + + + + +@code { + public string? Title { get; private set; } + MudForm _{itemnamelowercase}Form = new(); + private bool _saving = false; + private List? _breadcrumbItems; + private Create{itemname}Command _model = new(); + protected override Task OnInitializedAsync() + { + Title = L["New {itemname}"]; + _breadcrumbItems = new List + { + new BreadcrumbItem(L["Home"], href: "/"), + new BreadcrumbItem(L["{nameofPlural}"], href: "/pages/{nameofplurallowercase}"), + new BreadcrumbItem(L["Create {itemname}"], href:null, disabled:true) + }; + return Task.CompletedTask; + } + async Task OnSubmit() + { + try + { + _saving = true; + await _{itemnamelowercase}Form.Validate().ConfigureAwait(false); + if (!_{itemnamelowercase}Form.IsValid) + return; + var result = await Mediator.Send(_model); + result.Match( + data=> + { + Snackbar.Add(ConstantString.SaveSuccess, MudBlazor.Severity.Info); + Navigation.NavigateTo($"/pages/{nameofPlural}"); + }, + errors=> + { + Snackbar.Add(errors, MudBlazor.Severity.Error); + }); + } + finally + { + _saving = false; + } + } +} \ No newline at end of file diff --git a/src/Templates/Pages/.edit.razor.txt b/src/Templates/Pages/.edit.razor.txt new file mode 100644 index 0000000..f36dc0a --- /dev/null +++ b/src/Templates/Pages/.edit.razor.txt @@ -0,0 +1,92 @@ +@page "/pages/{nameofplurallowercase}/edit/{id:int}" +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Commands.Update +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Queries.GetById +@using CleanArchitecture.Blazor.Server.UI.Components.Fusion + +@inherits MudComponentBase +@inject IValidationService Validator +@inject IStringLocalizer<{nameofPlural}> L +@attribute [Authorize(Policy = Permissions.{nameofPlural}.Edit)] + +@Title + + +@if (_model != null) +{ + + + + @Title + + + + + + + {mudFormFieldDefinition} + + + + + @ConstantString.Save + + + } + + + +@code { + public string? Title { get; private set; } + [Parameter] + public int Id { get; set; } + MudForm _{itemnamelowercase}Form = new(); + private bool _saving = false; + private List? _breadcrumbItems; + private Update{itemname}Command? _model; + protected override async Task OnInitializedAsync() + { + Title = L["Edit {itemname}"]; + _breadcrumbItems = new List + { + new BreadcrumbItem(L["Home"], href: "/"), + new BreadcrumbItem(L["{nameofPlural}"], href: "/pages/{nameofplurallowercase}") + }; + var result = await Mediator.Send(new Get{itemname}ByIdQuery() { Id = Id }); + result.Map(data => + { + _model = Mapper.Map(data); + return data; + }).Match(data => + { + _breadcrumbItems.Add(new BreadcrumbItem(data.Name, href: $"/pages/{nameofplurallowercase}/edit/{Id}")); + }, errors => + { + Snackbar.Add($"{errors}", Severity.Error); + }); + + } + async Task OnSubmit() + { + try + { + _saving = true; + await _{itemnamelowercase}Form.Validate().ConfigureAwait(false); + if (!_{itemnamelowercase}Form.IsValid) + return; + var result = await Mediator.Send(_model); + result.Match( + data=> + { + Snackbar.Add(ConstantString.SaveSuccess, MudBlazor.Severity.Info); + }, + errors=> + { + Snackbar.Add(errors, MudBlazor.Severity.Error); + }); + } + finally + { + _saving = false; + } + } +} diff --git a/src/Templates/Pages/.formdialog.razor.txt b/src/Templates/Pages/.formdialog.razor.txt deleted file mode 100644 index d3fe021..0000000 --- a/src/Templates/Pages/.formdialog.razor.txt +++ /dev/null @@ -1,82 +0,0 @@ -@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Commands.AddEdit - -@inherits MudComponentBase -@inject IValidationService Validator -@inject IStringLocalizer<{nameofPlural}> L - - - - - - @*TODO: define mudform that should be edit fields, for example:*@ - {mudFormFieldDefinition} - - - - - @ConstantString.Cancel - @ConstantString.SaveAndNew - @ConstantString.Save - - - -@code { - MudForm? _form; - private bool _saving = false; - private bool _savingnew = false; - [CascadingParameter] - MudDialogInstance MudDialog { get; set; } = default!; - AddEdit{itemname}CommandValidator _modelValidator = new (); - [EditorRequired] [Parameter] public AddEdit{itemname}Command model { get; set; } = null!; - [Inject] private IMediator _mediator { get; set; } = default!; - async Task Submit() - { - try - { - _saving = true; - await _form!.Validate().ConfigureAwait(false); - if (!_form!.IsValid) - return; - var result = await _mediator.Send(model); - if (result.Succeeded) - { - MudDialog.Close(DialogResult.Ok(true)); - Snackbar.Add(ConstantString.SaveSuccess, MudBlazor.Severity.Info); - } - else - { - Snackbar.Add(result.ErrorMessage, MudBlazor.Severity.Error); - } - } - finally - { - _saving = false; - } - } - async Task SaveAndNew() - { - try - { - _savingnew = true; - await _form!.Validate().ConfigureAwait(false); - if (!_form!.IsValid) - return; - var result = await _mediator.Send(model); - if (result.Succeeded) - { - Snackbar.Add(ConstantString.SaveSuccess, MudBlazor.Severity.Info); - await Task.Delay(300); - model = new AddEdit{itemname}Command() { }; - } - else - { - Snackbar.Add(result.ErrorMessage, MudBlazor.Severity.Error); - } - } - finally - { - _savingnew = false; - } - } - void Cancel() => MudDialog.Cancel(); -} \ No newline at end of file diff --git a/src/Templates/Pages/.razor.txt b/src/Templates/Pages/.razor.txt index 12c904d..18fd625 100644 --- a/src/Templates/Pages/.razor.txt +++ b/src/Templates/Pages/.razor.txt @@ -1,6 +1,5 @@ -@page "/pages/{nameofPlural}" +@page "/pages/{nameofplurallowercase}" -@using BlazorDownloadFile @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Caching @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.DTOs @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Specifications @@ -9,179 +8,129 @@ @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Queries.Export @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Queries.Pagination @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Commands.AddEdit +@using CleanArchitecture.Blazor.Server.UI.Pages.{nameofPlural}.Components -@inherits FluxorComponent -@inject IJSRuntime JS @inject IStringLocalizer<{nameofPlural}> L +@inject BlazorDownloadFileService BlazorDownloadFileService + @attribute [Authorize(Policy = Permissions.{nameofPlural}.View)] @Title - + T="{itemname}Dto" + SelectOnRowClick="false" + RowClick="@(s=>OnDataGridRowClick(s.Item))" + @bind-SelectedItems="_selected{nameofPlural}" + Hover="true" @ref="_{nameofplurallowercase}Grid"> -
-
- -
- @Title - - -
-
-
- -
-
- - @ConstantString.Refresh - @if (_canCreate) - { - @ConstantString.New - @ConstantString.Clone - } - @if (_canDelete) - { - @ConstantString.Delete - } - @if (_canExport) - { - - @ConstantString.Export - - } - @if (_canImport) - { - - - - @if (_uploading) - { - - @ConstantString.Uploading - } - else - { - @ConstantString.Import - } - - - - } - - - @if (_canCreate) - { - @ConstantString.New - } - @if (_canDelete) - { - @ConstantString.Delete - } - -
- @if (_canSearch) - { - - } - -
-
+ + + + + @Title + + + + + + + + @ConstantString.Refresh + + @if (_accessRights.Create) + { + + @ConstantString.New + + } + + @if (_accessRights.Create) + { + @ConstantString.Clone + } + @if (_accessRights.Delete) + { + + @ConstantString.Delete + + } + @if (_accessRights.Export) + { + + @ConstantString.Export + + } + @if (_accessRights.Import) + { + + + + + @ConstantString.Import + + + + + } + + + + @if (_accessRights.Search) + { + + + } + + + - + - @if (_canEdit || _canDelete) + @if (_accessRights.Edit || _accessRights.Delete) { - - @if (_canEdit) + @if (_accessRights.Edit) { - @ConstantString.Edit + @ConstantString.Edit } - @if (_canDelete) + @if (_accessRights.Delete) { - @ConstantString.Delete + @ConstantString.Delete } } else { - - @ConstantString.NoAllowed - + + + @ConstantString.NoAllowed + + } - @*TODO: Define the fields that should be displayed in data table*@ {mudTdHeaderDefinition} @@ -200,56 +149,41 @@ @code { public string? Title { get; private set; } private int _defaultPageSize = 15; - private HashSet<{itemname}Dto> _selectedItems = new HashSet<{itemname}Dto>(); - private MudDataGrid<{itemname}Dto> _table = default!; - private {itemname}Dto _currentDto = new(); + private HashSet<{itemname}Dto> _selected{nameofPlural} = new HashSet<{itemname}Dto>(); + private MudDataGrid<{itemname}Dto> _{nameofplurallowercase}Grid = default!; + private {itemname}Dto _{itemnamelowercase}Dto = new(); private bool _loading; private bool _uploading; - private bool _downloading; private bool _exporting; - [Inject] - private IState UserProfileState { get; set; } = null!; - private UserProfile UserProfile => UserProfileState.Value.UserProfile; - - [Inject] - private IMediator Mediator { get; set; } = default!; - [Inject] - private IMapper Mapper { get; set; } = default!; [CascadingParameter] private Task AuthState { get; set; } = default!; + [CascadingParameter] + private UserProfile? UserProfile { get; set; } + - private {nameofPlural}WithPaginationQuery Query { get; set; } = new(); - [Inject] - private IBlazorDownloadFileService BlazorDownloadFileService { get; set; } = null!; - private bool _canSearch; - private bool _canCreate; - private bool _canEdit; - private bool _canDelete; - private bool _canImport; - private bool _canExport; + private {nameofPlural}WithPaginationQuery _{nameofplurallowercase}Query { get; set; } = new(); + private {nameofPlural}AccessRights _accessRights = new(); protected override async Task OnInitializedAsync() { - Title = L[_currentDto.GetClassDescription()]; - var state = await AuthState; - _canCreate = (await AuthService.AuthorizeAsync(state.User, Permissions.{nameofPlural}.Create)).Succeeded; - _canSearch = (await AuthService.AuthorizeAsync(state.User, Permissions.{nameofPlural}.Search)).Succeeded; - _canEdit = (await AuthService.AuthorizeAsync(state.User, Permissions.{nameofPlural}.Edit)).Succeeded; - _canDelete = (await AuthService.AuthorizeAsync(state.User, Permissions.{nameofPlural}.Delete)).Succeeded; - _canImport = (await AuthService.AuthorizeAsync(state.User, Permissions.{nameofPlural}.Import)).Succeeded; - _canExport = (await AuthService.AuthorizeAsync(state.User, Permissions.{nameofPlural}.Export)).Succeeded; + Title = L[_{itemnamelowercase}Dto.GetClassDescription()]; + _accessRights = await PermissionService.GetAccessRightsAsync<{nameofPlural}AccessRights>(); } + private async Task> ServerReload(GridState<{itemname}Dto> state) { try { _loading = true; - Query.CurrentUser = UserProfile; - Query.OrderBy = state.SortDefinitions.FirstOrDefault()?.SortBy ?? "Id"; - Query.SortDirection = state.SortDefinitions.FirstOrDefault()?.Descending ?? true ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString(); - Query.PageNumber = state.Page + 1; - Query.PageSize = state.PageSize; - var result = await Mediator.Send(Query).ConfigureAwait(false); + _{nameofplurallowercase}Query.CurrentUser = UserProfile; + var sortDefinition = state.SortDefinitions.FirstOrDefault(); + _{nameofplurallowercase}Query.OrderBy = sortDefinition?.SortBy ?? "Id"; + _{nameofplurallowercase}Query.SortDirection = (sortDefinition != null && sortDefinition.Descending) + ? SortDirection.Descending.ToString() + : SortDirection.Ascending.ToString(); + _{nameofplurallowercase}Query.PageNumber = state.Page + 1; + _{nameofplurallowercase}Query.PageSize = state.PageSize; + var result = await Mediator.Send(_{nameofplurallowercase}Query).ConfigureAwait(false); return new GridData<{itemname}Dto>() { TotalItems = result.TotalItems, Items = result.Items }; } finally @@ -260,134 +194,110 @@ } private async Task OnSearch(string text) { - _selectedItems = new(); - Query.Keyword = text; - await _table.ReloadServerData(); + _selected{nameofPlural}.Clear(); + _{nameofplurallowercase}Query.Keyword = text; + await _{nameofplurallowercase}Grid.ReloadServerData(); } - private async Task OnChangedListView({itemname}ListView listview) + private async Task OnListViewChanged({itemname}ListView listview) { - Query.ListView = listview; - await _table.ReloadServerData(); + _{nameofplurallowercase}Query.ListView = listview; + await _{nameofplurallowercase}Grid.ReloadServerData(); } private async Task OnRefresh() { {itemname}CacheKey.Refresh(); - _selectedItems = new(); - Query.Keyword = string.Empty; - await _table.ReloadServerData(); + _selected{nameofPlural}.Clear(); + _{nameofplurallowercase}Query.Keyword = string.Empty; + await _{nameofplurallowercase}Grid.ReloadServerData(); } - - private async Task OnCreate() + private Task ShowEditFormDialog(string title, AddEdit{itemname}Command command) { - var command = new AddEdit{itemname}Command(); - var parameters = new DialogParameters<_{itemname}FormDialog> + return DialogServiceHelper.ShowFormDialogAsync<{itemname}FormDialog, AddEdit{itemname}Command>( + title, + command, + async () => { - { x=>x.model,command }, - }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; - var dialog = DialogService.Show<_{itemname}FormDialog> - (L["Create a new item"], parameters, options); - var state = await dialog.Result; - if (!state.Canceled) - { - await _table.ReloadServerData(); - } + await _{nameofplurallowercase}Grid.ReloadServerData(); + _selected{nameofPlural}.Clear(); + }); } - private async Task OnClone() + private void OnDataGridRowClick({itemname}Dto dto) { - var copyitem = _selectedItems.First(); - var command = new AddEdit{itemname}Command(){ - Name = copyitem.Name, - Description = copyitem.Description, - }; - var parameters = new DialogParameters<_{itemname}FormDialog> - { - { x=>x.model,command }, - }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; - var dialog = DialogService.Show<_{itemname}FormDialog> - (L["Create a new item"], parameters, options); - var state = await dialog.Result; - if (!state.Canceled) - { - await _table.ReloadServerData(); - _selectedItems.Remove(copyitem); - } + Navigation.NavigateTo($"/pages/{nameofplurallowercase}/view/{dto.Id}"); } - private async Task OnEdit({itemname}Dto dto) + private Task OnCreate() { - var command = Mapper.Map(dto); - var parameters = new DialogParameters<_{itemname}FormDialog> - { - { x=>x.model,command }, - }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; - var dialog = DialogService.Show<_{itemname}FormDialog> - (L["Edit the item"], parameters, options); - var state = await dialog.Result; - if (!state.Canceled) + var command = new AddEdit{itemname}Command(); + return ShowEditFormDialog(L["New {itemname}"], command); + } + private Task OnClone{itemname}() + { + var dto = _selected{nameofPlural}.First(); + var command = new AddEdit{itemname}Command() { - await _table.ReloadServerData(); - } + {fieldAssignmentDefinition} + }; + return ShowEditFormDialog(L["Clone {itemname}"], command); + } + private Task OnEdit{itemname}({itemname}Dto dto) + { + //var command = Mapper.Map(dto); + //return ShowEditFormDialog(L["Edit {itemname}"], command); + Navigation.NavigateTo($"/pages/{nameofplurallowercase}/edit/{dto.Id}"); + return Task.CompletedTask; } - private async Task OnDelete({itemname}Dto dto) + private Task OnDelete{itemname}({itemname}Dto dto) { + var contentText = string.Format(ConstantString.DeleteConfirmation, dto.Name); var command = new Delete{itemname}Command(new int[] { dto.Id }); - var parameters = new DialogParameters - { - { x=>x.Command, command }, - { x=>x.ContentText, string.Format(ConstantString.DeleteConfirmation, dto.Name) } - }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.Small, FullWidth = true, DisableBackdropClick = true }; - var dialog = DialogService.Show(ConstantString.DeleteConfirmationTitle, parameters, options); - var state = await dialog.Result; - if (!state.Canceled) - { - await _table.ReloadServerData(); - _selectedItems.Remove(dto); - } + return Delete{nameofPlural}Internal(command, contentText); } - private async Task OnDeleteChecked() + private Task OnDeleteSelected{nameofPlural}() { - var command = new Delete{itemname}Command(_selectedItems.Select(x => x.Id).ToArray()); - var parameters = new DialogParameters + var contentText = string.Format(ConstantString.DeleteConfirmWithSelected, _selected{nameofPlural}.Count); + var command = new Delete{itemname}Command(_selected{nameofPlural}.Select(x => x.Id).ToArray()); + return Delete{nameofPlural}Internal(command, contentText); + } + + private Task Delete{nameofPlural}Internal(Delete{itemname}Command command, string contentText) + { + return DialogServiceHelper.ShowDeleteConfirmationDialogAsync( + command, + ConstantString.DeleteConfirmationTitle, + contentText, + async () => { - { x=>x.Command, command }, - { x=>x.ContentText, string.Format(ConstantString.DeleteConfirmWithSelected,_selectedItems.Count) } - }; - var options = new DialogOptions { CloseButton = true, MaxWidth = MaxWidth.ExtraSmall, FullWidth = true, DisableBackdropClick = true }; - var dialog = DialogService.Show(ConstantString.DeleteConfirmationTitle, parameters, options); - var state = await dialog.Result; - if (!state.Canceled) - { - await _table.ReloadServerData(); - _selectedItems = new(); - } + await _{nameofplurallowercase}Grid.ReloadServerData(); + _selected{nameofPlural}.Clear(); + }); } + private async Task OnExport() { _exporting = true; var request = new Export{nameofPlural}Query() { - Keyword = Query.Keyword, + Keyword = _{nameofplurallowercase}Query.Keyword, CurrentUser = UserProfile, - ListView = Query.ListView, - OrderBy = _table.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", - SortDirection = (_table.SortDefinitions.Values.FirstOrDefault()?.Descending ?? true) ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString() + ListView = _{nameofplurallowercase}Query.ListView, + OrderBy = _{nameofplurallowercase}Grid.SortDefinitions.Values.FirstOrDefault()?.SortBy ?? "Id", + SortDirection = (_{nameofplurallowercase}Grid.SortDefinitions.Values.FirstOrDefault()?.Descending ?? true) ? SortDirection.Descending.ToString() : SortDirection.Ascending.ToString() }; var result = await Mediator.Send(request); - if (result.Succeeded) - { - var downloadresult = await BlazorDownloadFileService.DownloadFile($"{L["{nameofPlural}"]}.xlsx", result.Data, contentType:"application/octet-stream"); - Snackbar.Add($"{ConstantString.ExportSuccess}", MudBlazor.Severity.Info); - } - else - { - Snackbar.Add($"{result.ErrorMessage}", MudBlazor.Severity.Error); - } + await result.MatchAsync( + async data => + { + await BlazorDownloadFileService.DownloadFileAsync($"{L["{nameofPlural}"]}.xlsx", result.Data, contentType:"application/octet-stream"); + Snackbar.Add($"{ConstantString.ExportSuccess}", MudBlazor.Severity.Info); + }, + errors => + { + Snackbar.Add($"{errors}", MudBlazor.Severity.Error); + return Task.CompletedTask; + }); _exporting = false; } private async Task OnImportData(IBrowserFile file) @@ -397,19 +307,17 @@ await file.OpenReadStream().CopyToAsync(stream); var command = new Import{nameofPlural}Command(file.Name, stream.ToArray()); var result = await Mediator.Send(command); - if (result.Succeeded) - { - await _table.ReloadServerData(); - Snackbar.Add($"{ConstantString.ImportSuccess}", MudBlazor.Severity.Info); - } - else - { - foreach (var msg in result.Errors) + await result.MatchAsync( + async data => { - Snackbar.Add($"{msg}", MudBlazor.Severity.Error); - } - } + await _{nameofplurallowercase}Grid.ReloadServerData(); + Snackbar.Add($"{ConstantString.ImportSuccess}", MudBlazor.Severity.Info); + }, errors => + { + Snackbar.Add($"{errors}", MudBlazor.Severity.Error); + return Task.CompletedTask; + }); _uploading = false; } -} \ No newline at end of file +} diff --git a/src/Templates/Pages/.view.razor.txt b/src/Templates/Pages/.view.razor.txt new file mode 100644 index 0000000..9e960fd --- /dev/null +++ b/src/Templates/Pages/.view.razor.txt @@ -0,0 +1,74 @@ +@page "/pages/{nameofplurallowercase}/view/{id:int}" +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Commands.Delete +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.DTOs +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Queries.GetById +@inherits MudComponentBase +@inject IStringLocalizer<{nameofPlural}> L +@attribute [Authorize(Policy = Permissions.{nameofPlural}.View)] +@Title + + +@if (_model != null) +{ + + + + @Title + + + + + {readonlyFieldDefinition} + + + + + } + + + +@code { + public string? Title { get; private set; } + [Parameter] + public int Id { get; set; } + private List? _breadcrumbItems; + private {itemname}Dto? _model; + protected override async Task OnInitializedAsync() + { + Title = L["{itemname}"]; + _breadcrumbItems = new List + { + new BreadcrumbItem(L["Home"], href: "/"), + new BreadcrumbItem(L["{nameofPlural}"], href: "/pages/{nameofplurallowercase}") + }; + var result = await Mediator.Send(new Get{itemname}ByIdQuery() { Id = Id }); + result.Map(data => + { + _model = data; + return data; + }).Match(data => + { + _breadcrumbItems.Add(new BreadcrumbItem(data.Name, null, disabled: true)); + }, errors => + { + Snackbar.Add(errors, MudBlazor.Severity.Error); + }); + + } + void GoEdit() + { + Navigation.NavigateTo($"/pages/{nameofPlural}/edit/{Id}"); + } + async Task Delete() + { + var contentText = string.Format(ConstantString.DeleteConfirmation, _model.Name); + var command = new Delete{itemname}Command(new int[] { _model.Id }); + await DialogServiceHelper.ShowDeleteConfirmationDialogAsync(command, ConstantString.DeleteConfirmationTitle, contentText, async () => + { + await InvokeAsync(() => + { + Navigation.NavigateTo($"/pages/{nameofPlural}"); + }); + }); + } +} diff --git a/src/Templates/Pages/Components/.advancedsearchcomponent.razor.txt b/src/Templates/Pages/Components/.advancedsearchcomponent.razor.txt index e1bffd8..48a3b05 100644 --- a/src/Templates/Pages/Components/.advancedsearchcomponent.razor.txt +++ b/src/Templates/Pages/Components/.advancedsearchcomponent.razor.txt @@ -1,13 +1,12 @@ @using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Queries.Pagination @inject IStringLocalizer<{nameofPlural}> L - + - @*TODO: define advanced search query fields, for example:*@ - @* + @* @@ -17,7 +16,7 @@ @code { - [EditorRequired][Parameter] public {nameofPlural}WithPaginationQuery TRequest { get; set; } = null!; + [EditorRequired][Parameter] public {nameofPlural}WithPaginationQuery {nameofPlural}Query { get; set; } = null!; [EditorRequired][Parameter] public EventCallback OnConditionChanged { get; set; } private bool _advancedSearchExpanded; private async Task TextChanged(string str) diff --git a/src/Templates/Pages/Components/.formdialog.razor.txt b/src/Templates/Pages/Components/.formdialog.razor.txt new file mode 100644 index 0000000..28bf211 --- /dev/null +++ b/src/Templates/Pages/Components/.formdialog.razor.txt @@ -0,0 +1,78 @@ +@using CleanArchitecture.Blazor.Application.Features.{nameofPlural}.Commands.AddEdit + +@inherits MudComponentBase +@inject IValidationService Validator +@inject IStringLocalizer<{nameofPlural}> L + + + + + + {mudFormFieldDefinition} + + + + + @ConstantString.Cancel + @ConstantString.SaveAndNew + @ConstantString.Save + + + +@code { + MudForm _{itemnamelowercase}Form = new(); + private bool _saving = false; + private bool _savingnew = false; + [CascadingParameter] + IMudDialogInstance MudDialog { get; set; } = default!; + [EditorRequired] [Parameter] public AddEdit{itemname}Command _model { get; set; } = null!; + async Task OnSubmit() + { + try + { + _saving = true; + await _{itemnamelowercase}Form.Validate().ConfigureAwait(false); + if (!_{itemnamelowercase}Form.IsValid) + return; + var result = await Mediator.Send(_model); + result.Match(data => + { + MudDialog.Close(DialogResult.Ok(true)); + Snackbar.Add(ConstantString.SaveSuccess, MudBlazor.Severity.Info); + }, errors => + { + Snackbar.Add(errors, MudBlazor.Severity.Error); + }); + } + finally + { + _saving = false; + } + } + async Task OnSaveAndNew() + { + try + { + _savingnew = true; + await _{itemnamelowercase}Form.Validate().ConfigureAwait(false); + if (!_{itemnamelowercase}Form.IsValid) + return; + var result = await Mediator.Send(_model); + await result.MatchAsync(async data => + { + Snackbar.Add(ConstantString.SaveSuccess, MudBlazor.Severity.Info); + await Task.Delay(300); + _model = new AddEdit{itemname}Command() { }; + }, errors => + { + Snackbar.Add(errors, MudBlazor.Severity.Error); + return Task.CompletedTask; + }); + } + finally + { + _savingnew = false; + } + } + void Cancel() => MudDialog.Cancel(); +} \ No newline at end of file diff --git a/src/Templates/Persistence/Configurations/.configuration.cs.txt b/src/Templates/Persistence/Configurations/.configuration.cs.txt index ef9a712..4aa2508 100644 --- a/src/Templates/Persistence/Configurations/.configuration.cs.txt +++ b/src/Templates/Persistence/Configurations/.configuration.cs.txt @@ -1,8 +1,18 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Configures the properties and behaviors for the `{itemname}` entity in the +// database. Specifies property constraints such as maximum length and required fields. +// +//------------------------------------------------------------------------------ -using System.Text.Json; -using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace {namespace}; @@ -12,7 +22,7 @@ public class {itemname}Configuration : IEntityTypeConfiguration<{itemname}> { public void Configure(EntityTypeBuilder<{itemname}> builder) { - builder.Property(t => t.Name).HasMaxLength(50).IsRequired(); + {entityTypeBuilderConfirmation} builder.Ignore(e => e.DomainEvents); } } diff --git a/src/Templates/Queries/Export/.cs.txt b/src/Templates/Queries/Export/.cs.txt index 23e9b05..58b20eb 100644 --- a/src/Templates/Queries/Export/.cs.txt +++ b/src/Templates/Queries/Export/.cs.txt @@ -1,34 +1,55 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines a query to export {itemnamelowercase} data to an Excel file. This query +// applies advanced filtering options and generates an Excel file with +// the specified {itemnamelowercase} details. +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings using {selectns}.{nameofPlural}.DTOs; +using {selectns}.{nameofPlural}.Caching; using {selectns}.{nameofPlural}.Specifications; -using {selectns}.{nameofPlural}.Queries.Pagination; namespace {namespace}; -public class Export{nameofPlural}Query : {itemname}AdvancedFilter, IRequest> +public class Export{nameofPlural}Query : {itemname}AdvancedFilter, ICacheableRequest> { public {itemname}AdvancedSpecification Specification => new {itemname}AdvancedSpecification(this); + public IEnumerable? Tags => {itemname}CacheKey.Tags; + public override string ToString() + { + return $"Listview:{ListView}:{CurrentUser?.UserId}, Search:{Keyword}, {OrderBy}, {SortDirection}"; + } + public string CacheKey => {itemname}CacheKey.GetExportCacheKey($"{this}"); } public class Export{nameofPlural}QueryHandler : IRequestHandler> { - private readonly IApplicationDbContext _context; private readonly IMapper _mapper; + private readonly IApplicationDbContext _context; private readonly IExcelService _excelService; private readonly IStringLocalizer _localizer; private readonly {itemname}Dto _dto = new(); public Export{nameofPlural}QueryHandler( - IApplicationDbContext context, IMapper mapper, + IApplicationDbContext context, IExcelService excelService, IStringLocalizer localizer ) { - _context = context; _mapper = mapper; + _context = context; _excelService = excelService; _localizer = localizer; } @@ -43,7 +64,6 @@ public class Export{nameofPlural}QueryHandler : var result = await _excelService.ExportAsync(data, new Dictionary>() { - // TODO: Define the fields that should be exported, for example: {exportFuncExpression} } , _localizer[_dto.GetClassDescription()]); diff --git a/src/Templates/Queries/GetAll/.cs.txt b/src/Templates/Queries/GetAll/.cs.txt index 82e2f3e..fef6d10 100644 --- a/src/Templates/Queries/GetAll/.cs.txt +++ b/src/Templates/Queries/GetAll/.cs.txt @@ -1,5 +1,20 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines a query to retrieve all {itemnamelowercase}s from the database. The result +// is cached to improve performance and reduce database load for repeated +// queries. +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings using {selectns}.{nameofPlural}.DTOs; using {selectns}.{nameofPlural}.Caching; @@ -9,7 +24,7 @@ namespace {namespace}; public class GetAll{nameofPlural}Query : ICacheableRequest> { public string CacheKey => {itemname}CacheKey.GetAllCacheKey; - public MemoryCacheEntryOptions? Options => {itemname}CacheKey.MemoryCacheEntryOptions; + public IEnumerable? Tags => {itemname}CacheKey.Tags; } public class GetAll{nameofPlural}QueryHandler : @@ -17,25 +32,19 @@ public class GetAll{nameofPlural}QueryHandler : { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly IStringLocalizer _localizer; - public GetAll{nameofPlural}QueryHandler( - IApplicationDbContext context, IMapper mapper, - IStringLocalizer localizer - ) + IApplicationDbContext context) { - _context = context; _mapper = mapper; - _localizer = localizer; + _context = context; } public async Task> Handle(GetAll{nameofPlural}Query request, CancellationToken cancellationToken) { - var data = await _context.{nameofPlural} - .ProjectTo<{itemname}Dto>(_mapper.ConfigurationProvider) - .AsNoTracking() - .ToListAsync(cancellationToken); + var data = await _context.{nameofPlural}.ProjectTo<{itemname}Dto>(_mapper.ConfigurationProvider) + .AsNoTracking() + .ToListAsync(cancellationToken); return data; } } diff --git a/src/Templates/Queries/GetById/.cs.txt b/src/Templates/Queries/GetById/.cs.txt index 3128da6..96aaa2e 100644 --- a/src/Templates/Queries/GetById/.cs.txt +++ b/src/Templates/Queries/GetById/.cs.txt @@ -1,5 +1,19 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines a query to retrieve a {itemnamelowercase} by its ID. The result is cached +// to optimize performance for repeated retrievals of the same {itemnamelowercase}. +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings using {selectns}.{nameofPlural}.DTOs; using {selectns}.{nameofPlural}.Caching; @@ -7,36 +21,31 @@ using {selectns}.{nameofPlural}.Specifications; namespace {namespace}; -public class Get{itemname}ByIdQuery : ICacheableRequest<{itemname}Dto> +public class Get{itemname}ByIdQuery : ICacheableRequest> { public required int Id { get; set; } public string CacheKey => {itemname}CacheKey.GetByIdCacheKey($"{Id}"); - public MemoryCacheEntryOptions? Options => {itemname}CacheKey.MemoryCacheEntryOptions; + public IEnumerable? Tags => {itemname}CacheKey.Tags; } public class Get{itemname}ByIdQueryHandler : - IRequestHandler + IRequestHandler> { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly IStringLocalizer _localizer; - public Get{itemname}ByIdQueryHandler( - IApplicationDbContext context, IMapper mapper, - IStringLocalizer localizer - ) + IApplicationDbContext context) { - _context = context; _mapper = mapper; - _localizer = localizer; + _context = context; } - public async Task<{itemname}Dto> Handle(Get{itemname}ByIdQuery request, CancellationToken cancellationToken) + public async Task> Handle(Get{itemname}ByIdQuery request, CancellationToken cancellationToken) { var data = await _context.{nameofPlural}.ApplySpecification(new {itemname}ByIdSpecification(request.Id)) - .ProjectTo<{itemname}Dto>(_mapper.ConfigurationProvider) - .FirstAsync(cancellationToken) ?? throw new NotFoundException($"{itemname} with id: [{request.Id}] not found."); - return data; + .ProjectTo<{itemname}Dto>(_mapper.ConfigurationProvider) + .FirstAsync(cancellationToken) ?? throw new NotFoundException($"{itemname} with id: [{request.Id}] not found."); + return await Result<{itemname}Dto>.SuccessAsync(data); } } diff --git a/src/Templates/Queries/Pagination/.cs.txt b/src/Templates/Queries/Pagination/.cs.txt index 7baf3a4..c749f87 100644 --- a/src/Templates/Queries/Pagination/.cs.txt +++ b/src/Templates/Queries/Pagination/.cs.txt @@ -1,5 +1,19 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines a query for retrieving {itemnamelowercase}s with pagination and filtering +// options. The result is cached to enhance performance for repeated queries. +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings using {selectns}.{nameofPlural}.DTOs; using {selectns}.{nameofPlural}.Caching; @@ -11,10 +25,10 @@ public class {nameofPlural}WithPaginationQuery : {itemname}AdvancedFilter, ICach { public override string ToString() { - return $"Listview:{ListView}, Search:{Keyword}, {OrderBy}, {SortDirection}, {PageNumber}, {PageSize}"; + return $"Listview:{ListView}:{CurrentUser?.UserId}, Search:{Keyword}, {OrderBy}, {SortDirection}, {PageNumber}, {PageSize}"; } public string CacheKey => {itemname}CacheKey.GetPaginationCacheKey($"{this}"); - public MemoryCacheEntryOptions? Options => {itemname}CacheKey.MemoryCacheEntryOptions; + public IEnumerable? Tags => {itemname}CacheKey.Tags; public {itemname}AdvancedSpecification Specification => new {itemname}AdvancedSpecification(this); } @@ -23,23 +37,22 @@ public class {nameofPlural}WithPaginationQueryHandler : { private readonly IApplicationDbContext _context; private readonly IMapper _mapper; - private readonly IStringLocalizer<{nameofPlural}WithPaginationQueryHandler> _localizer; - public {nameofPlural}WithPaginationQueryHandler( - IApplicationDbContext context, IMapper mapper, - IStringLocalizer<{nameofPlural}WithPaginationQueryHandler> localizer - ) + IApplicationDbContext context) { - _context = context; _mapper = mapper; - _localizer = localizer; + _context = context; } public async Task> Handle({nameofPlural}WithPaginationQuery request, CancellationToken cancellationToken) { var data = await _context.{nameofPlural}.OrderBy($"{request.OrderBy} {request.SortDirection}") - .ProjectToPaginatedDataAsync<{itemname}, {itemname}Dto>(request.Specification, request.PageNumber, request.PageSize, _mapper.ConfigurationProvider, cancellationToken); + .ProjectToPaginatedDataAsync<{itemname}, {itemname}Dto>(request.Specification, + request.PageNumber, + request.PageSize, + _mapper.ConfigurationProvider, + cancellationToken); return data; } } \ No newline at end of file diff --git a/src/Templates/Security/.cs.txt b/src/Templates/Security/.cs.txt new file mode 100644 index 0000000..bbf1df2 --- /dev/null +++ b/src/Templates/Security/.cs.txt @@ -0,0 +1,49 @@ +//------------------------------------------------------------------------------ +// +// CleanArchitecture.Blazor - MIT Licensed. +// Author: {author} +// Created/Modified: {createddate} +// Defines permission constants for {itemnamelowercase} operations (view, create, edit, delete, etc.). +// +//------------------------------------------------------------------------------ + + + +namespace CleanArchitecture.Blazor.Application.Common.Security; + +public static partial class Permissions +{ + [DisplayName("{itemname} Permissions")] + [Description("Set permissions for {itemnamelowercase} operations.")] + public static class {nameofPlural} + { + [Description("Allows viewing {itemnamelowercase} details.")] + public const string View = "Permissions.{nameofPlural}.View"; + [Description("Allows creating {itemnamelowercase} records.")] + public const string Create = "Permissions.{nameofPlural}.Create"; + [Description("Allows modifying existing {itemnamelowercase} details.")] + public const string Edit = "Permissions.{nameofPlural}.Edit"; + [Description("Allows deleting {itemnamelowercase} records.")] + public const string Delete = "Permissions.{nameofPlural}.Delete"; + [Description("Allows printing {itemnamelowercase} details.")] + public const string Print = "Permissions.{nameofPlural}.Print"; + [Description("Allows searching {itemnamelowercase} records.")] + public const string Search = "Permissions.{nameofPlural}.Search"; + [Description("Allows exporting {itemnamelowercase} records.")] + public const string Export = "Permissions.{nameofPlural}.Export"; + [Description("Allows importing {itemnamelowercase} records.")] + public const string Import = "Permissions.{nameofPlural}.Import"; + } +} + +public class {nameofPlural}AccessRights +{ + public bool View { get; set; } + public bool Create { get; set; } + public bool Edit { get; set; } + public bool Delete { get; set; } + public bool Print { get; set; } + public bool Search { get; set; } + public bool Export { get; set; } + public bool Import { get; set; } +} diff --git a/src/Templates/Specifications/AdvancedFilter.cs.txt b/src/Templates/Specifications/AdvancedFilter.cs.txt index 3f03e93..a40a396 100644 --- a/src/Templates/Specifications/AdvancedFilter.cs.txt +++ b/src/Templates/Specifications/AdvancedFilter.cs.txt @@ -1,5 +1,27 @@ -namespace {selectns}.{nameofPlural}.Specifications; +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines the available views for filtering {itemnamelowercase}s and provides advanced +// filtering options for {itemnamelowercase} lists. This includes pagination and various +// filters such as view types and user-specific filters. +// +//------------------------------------------------------------------------------ +#nullable enable #nullable disable warnings + +namespace {namespace}; + +#nullable disable warnings +/// +/// Specifies the different views available for the {itemname} list. +/// public enum {itemname}ListView { [Description("All")] @@ -7,11 +29,13 @@ public enum {itemname}ListView [Description("My")] My, [Description("Created Toady")] - CreatedToday, + TODAY, [Description("Created within the last 30 days")] - Created30Days + LAST_30_DAYS } - +/// +/// A class for applying advanced filtering options to {itemname} lists. +/// public class {itemname}AdvancedFilter: PaginationFilter { public {itemname}ListView ListView { get; set; } = {itemname}ListView.All; diff --git a/src/Templates/Specifications/AdvancedSpecification.cs.txt b/src/Templates/Specifications/AdvancedSpecification.cs.txt new file mode 100644 index 0000000..105af5d --- /dev/null +++ b/src/Templates/Specifications/AdvancedSpecification.cs.txt @@ -0,0 +1,38 @@ +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines a specification for applying advanced filtering options to the +// {itemname} entity, supporting different views and keyword-based searches. +// +//------------------------------------------------------------------------------ +#nullable enable +#nullable disable warnings + +namespace {namespace}; +#nullable disable warnings +/// +/// Specification class for advanced filtering of {nameofPlural}. +/// +public class {itemname}AdvancedSpecification : Specification<{itemname}> +{ + public {itemname}AdvancedSpecification({itemname}AdvancedFilter filter) + { + DateTime today = DateTime.UtcNow; + var todayrange = today.GetDateRange({itemname}ListView.TODAY.ToString(), filter.CurrentUser.LocalTimeOffset); + var last30daysrange = today.GetDateRange({itemname}ListView.LAST_30_DAYS.ToString(),filter.CurrentUser.LocalTimeOffset); + + Query.Where(q => q.Name != null) + .Where(filter.Keyword,!string.IsNullOrEmpty(filter.Keyword)) + .Where(q => q.CreatedBy == filter.CurrentUser.UserId, filter.ListView == {itemname}ListView.My && filter.CurrentUser is not null) + .Where(x => x.Created >= todayrange.Start && x.Created < todayrange.End.AddDays(1), filter.ListView == {itemname}ListView.TODAY) + .Where(x => x.Created >= last30daysrange.Start, filter.ListView == {itemname}ListView.LAST_30_DAYS); + + } +} diff --git a/src/Templates/Specifications/AdvancedSpecification.txt b/src/Templates/Specifications/AdvancedSpecification.txt deleted file mode 100644 index 7e32161..0000000 --- a/src/Templates/Specifications/AdvancedSpecification.txt +++ /dev/null @@ -1,23 +0,0 @@ -namespace {selectns}.{nameofPlural}.Specifications; -#nullable disable warnings -public class {itemname}AdvancedSpecification : Specification<{itemname}> -{ - public {itemname}AdvancedSpecification({itemname}AdvancedFilter filter) - { - var today = DateTime.Now.ToUniversalTime().Date; - var start = Convert.ToDateTime(today.ToString("yyyy-MM-dd", CultureInfo.CurrentCulture) + " 00:00:00", - CultureInfo.CurrentCulture); - var end = Convert.ToDateTime(today.ToString("yyyy-MM-dd", CultureInfo.CurrentCulture) + " 23:59:59", - CultureInfo.CurrentCulture); - var last30day = Convert.ToDateTime( - today.AddDays(-30).ToString("yyyy-MM-dd", CultureInfo.CurrentCulture) + " 00:00:00", - CultureInfo.CurrentCulture); - - Query.Where(q => q.Name != null) - .Where(q => q.Name!.Contains(filter.Keyword) || q.Description!.Contains(filter.Keyword), !string.IsNullOrEmpty(filter.Keyword)) - .Where(q => q.CreatedBy == filter.CurrentUser.UserId, filter.ListView == {itemname}ListView.My && filter.CurrentUser is not null) - .Where(q => q.Created >= start && q.Created <= end, filter.ListView == {itemname}ListView.CreatedToday) - .Where(q => q.Created >= last30day, filter.ListView == {itemname}ListView.Created30Days); - - } -} diff --git a/src/Templates/Specifications/ByIdSpecification.cs.txt b/src/Templates/Specifications/ByIdSpecification.cs.txt index 62d7b44..217e17d 100644 --- a/src/Templates/Specifications/ByIdSpecification.cs.txt +++ b/src/Templates/Specifications/ByIdSpecification.cs.txt @@ -1,5 +1,24 @@ -namespace {selectns}.{nameofPlural}.Specifications; +//------------------------------------------------------------------------------ +// +// This file is part of the CleanArchitecture.Blazor project. +// Licensed to the .NET Foundation under the MIT license. +// See the LICENSE file in the project root for more information. +// +// Author: {author} +// Created Date: {createddate} +// Last Modified: {createddate} +// Description: +// Defines a specification for filtering a {itemname} entity by its ID. +// +//------------------------------------------------------------------------------ +#nullable enable #nullable disable warnings + +namespace {namespace}; +#nullable disable warnings +/// +/// Specification class for filtering {nameofPlural} by their ID. +/// public class {itemname}ByIdSpecification : Specification<{itemname}> { public {itemname}ByIdSpecification(int id) diff --git a/src/source.extension.vsixmanifest b/src/source.extension.vsixmanifest index 019bb78..3f4e671 100644 --- a/src/source.extension.vsixmanifest +++ b/src/source.extension.vsixmanifest @@ -1,29 +1,29 @@  - - - CleanArchitecture CodeGenerator For Blazor App - The fastest and easiest way to generate application features code that up to clean architecture principles for Blazor Server Application - https://marketplace.visualstudio.com/items?itemName=CleanArchitecture.CodeGenerator - Resources\LICENSE - https://raw.githubusercontent.com/neozhu/CleanCleanArchitectureCodeGenerator/main/README.md - Resources\logo.png - Resources\logo.png - template, CleanArchitecture, CodeGenerator - true - - - - x86 - - - amd64 - - - - - - - - + + + CleanArchitecture CodeGenerator For Blazor App + The fastest and easiest way to generate application features code that up to clean architecture principles for Blazor Server Application + https://marketplace.visualstudio.com/items?itemName=CleanArchitecture.CodeGenerator + Resources\LICENSE + https://raw.githubusercontent.com/neozhu/CleanCleanArchitectureCodeGenerator/main/README.md + Resources\logo.png + Resources\logo.png + template, CleanArchitecture, CodeGenerator + true + + + + x86 + + + amd64 + + + + + + + +