diff --git a/zero.Core/Applications/ApplicationStore.cs b/zero.Core/Applications/ApplicationStore.cs index 4e4debf4..9e989250 100644 --- a/zero.Core/Applications/ApplicationStore.cs +++ b/zero.Core/Applications/ApplicationStore.cs @@ -10,13 +10,13 @@ public class ApplicationStore : EntityStore, IApplicationStore } - protected override void ValidationRules(ZeroValidator validator) - { - validator.RuleFor(x => x.Name).NotEmpty().Length(2, 50); - validator.RuleFor(x => x.FullName).MaximumLength(120); - validator.RuleFor(x => x.Email).Email().NotEmpty().MaximumLength(120); - validator.RuleFor(x => x.Domains).NotEmpty(); - } + //protected override void ValidationRules(ZeroValidator validator) + //{ + // validator.RuleFor(x => x.Name).NotEmpty().Length(2, 50); + // validator.RuleFor(x => x.FullName).MaximumLength(120); + // validator.RuleFor(x => x.Email).Email().NotEmpty().MaximumLength(120); + // validator.RuleFor(x => x.Domains).NotEmpty(); + //} } public interface IApplicationStore : IEntityStore diff --git a/zero.Core/Configuration/Integrations/IntegrationStore.cs b/zero.Core/Configuration/Integrations/IntegrationStore.cs index d00248ca..68009cc2 100644 --- a/zero.Core/Configuration/Integrations/IntegrationStore.cs +++ b/zero.Core/Configuration/Integrations/IntegrationStore.cs @@ -151,7 +151,7 @@ public class IntegrationStore : IIntegrationStore /// protected virtual async Task> Save(Integration model, bool create = false) { - return await Operations.Create(model, async (x, ctx) => await Validate(x, create)); + return await Operations.Create(model, async x => await Validate(x, create)); } } diff --git a/zero.Core/Localization/CountryStore.cs b/zero.Core/Localization/CountryStore.cs index 4b66c357..19dd07ed 100644 --- a/zero.Core/Localization/CountryStore.cs +++ b/zero.Core/Localization/CountryStore.cs @@ -1,16 +1,8 @@ -using FluentValidation.Results; - -namespace zero.Localization; +namespace zero.Localization; public class CountryStore : EntityStore, ICountryStore { public CountryStore(IStoreContext context) : base(context) { } - - - public override Task Validate(ZeroValidationContext ctx, Country model) - { - return new CountryValidator(ctx).ValidateAsync(model); - } } diff --git a/zero.Core/Localization/CountryValidator.cs b/zero.Core/Localization/CountryValidator.cs index 520f61fd..f9d572cd 100644 --- a/zero.Core/Localization/CountryValidator.cs +++ b/zero.Core/Localization/CountryValidator.cs @@ -4,9 +4,9 @@ namespace zero.Localization; public class CountryValidator : ZeroValidator { - public CountryValidator(ZeroValidationContext ctx) + public CountryValidator(IZeroStore store) { - RuleFor(x => x.Code).NotEmpty().Length(2).Unique(ctx); + RuleFor(x => x.Code).NotEmpty().Length(2).Unique(store); RuleFor(x => x.Name).NotEmpty().Length(2, 120); } } \ No newline at end of file diff --git a/zero.Core/Localization/LanguageStore.cs b/zero.Core/Localization/LanguageStore.cs index 9a887230..adcaf2f2 100644 --- a/zero.Core/Localization/LanguageStore.cs +++ b/zero.Core/Localization/LanguageStore.cs @@ -1,16 +1,8 @@ -using FluentValidation.Results; - -namespace zero.Localization; +namespace zero.Localization; public class LanguageStore : EntityStore, ILanguageStore { public LanguageStore(IStoreContext context) : base(context) { } - - - public override Task Validate(ZeroValidationContext ctx, Language model) - { - return new LanguageValidator(ctx).ValidateAsync(model); - } } diff --git a/zero.Core/Localization/LanguageValidator.cs b/zero.Core/Localization/LanguageValidator.cs index b75eb036..606cfaf6 100644 --- a/zero.Core/Localization/LanguageValidator.cs +++ b/zero.Core/Localization/LanguageValidator.cs @@ -4,15 +4,15 @@ namespace zero.Localization; public class LanguageValidator : ZeroValidator { - public LanguageValidator(ZeroValidationContext ctx) + public LanguageValidator(IZeroStore store) { RuleFor(x => x.Name).NotEmpty().Length(2, 60); RuleFor(x => x.Code).NotEmpty().Length(2, 10).Culture(); - RuleFor(x => x.IsDefault).Unique(ctx).When(x => x.IsDefault).WithMessage("@language.errors.default_unique"); - RuleFor(x => x.IsDefault).ExpectAnyUnique(ctx, expectedValue: true).When(x => !x.IsDefault).WithMessage("@language.errors.needs_default"); + RuleFor(x => x.IsDefault).Unique(store).When(x => x.IsDefault).WithMessage("@language.errors.default_unique"); + RuleFor(x => x.IsDefault).ExpectAnyUnique(store, expectedValue: true).When(x => !x.IsDefault).WithMessage("@language.errors.needs_default"); RuleFor(x => x.InheritedLanguageId).Must((entity, value) => !entity.Id.Equals(value, StringComparison.InvariantCultureIgnoreCase)).When(x => !x.Id.IsNullOrEmpty()).WithMessage("@language.errors.fallback_invalid"); RuleFor(x => x.InheritedLanguageId).Equal((string)null).When(x => x.IsDefault).WithMessage("@language.errors.default_no_fallback"); - RuleFor(x => x.InheritedLanguageId).Exists(ctx); + RuleFor(x => x.InheritedLanguageId).Exists(store); RuleFor(x => x.IsOptional).Equal(false).When(x => x.IsDefault).WithMessage("@language.errors.default_not_optional"); } } \ No newline at end of file diff --git a/zero.Core/Localization/TranslationStore.cs b/zero.Core/Localization/TranslationStore.cs index a9716d28..74368243 100644 --- a/zero.Core/Localization/TranslationStore.cs +++ b/zero.Core/Localization/TranslationStore.cs @@ -1,6 +1,4 @@ -using FluentValidation.Results; - -namespace zero.Localization; +namespace zero.Localization; public class TranslationStore : EntityStore, ITranslationStore { @@ -12,12 +10,6 @@ public class TranslationStore : EntityStore, ITranslationStore { return (await Load(id))?.Value; } - - - public override Task Validate(ZeroValidationContext ctx, Translation model) - { - return new TranslationValidator(ctx).ValidateAsync(model); - } } diff --git a/zero.Core/Localization/TranslationValidator.cs b/zero.Core/Localization/TranslationValidator.cs index 1230c6bd..e6fc3317 100644 --- a/zero.Core/Localization/TranslationValidator.cs +++ b/zero.Core/Localization/TranslationValidator.cs @@ -4,9 +4,9 @@ namespace zero.Localization; public class TranslationValidator : ZeroValidator { - public TranslationValidator(ZeroValidationContext ctx) + public TranslationValidator(IZeroStore store) { - RuleFor(x => x.Key).NotEmpty().Length(2, 300).Unique(ctx); + RuleFor(x => x.Key).NotEmpty().Length(2, 300).Unique(store); RuleFor(x => x.Value).MaximumLength(10 * 1000); } } \ No newline at end of file diff --git a/zero.Core/Localization/ZeroLocalizationModule.cs b/zero.Core/Localization/ZeroLocalizationModule.cs index 1137b155..6701a671 100644 --- a/zero.Core/Localization/ZeroLocalizationModule.cs +++ b/zero.Core/Localization/ZeroLocalizationModule.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Configuration; +using FluentValidation; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; namespace zero.Localization; @@ -13,5 +14,9 @@ internal class ZeroLocalizationModule : ZeroModule services.AddScoped(); services.AddScoped(); services.AddScoped(); + + services.AddScoped, CountryValidator>(); + services.AddScoped, LanguageValidator>(); + services.AddScoped, TranslationValidator>(); } } \ No newline at end of file diff --git a/zero.Core/Mails/MailTemplateValidator.cs b/zero.Core/Mails/MailTemplateValidator.cs index f72cd166..08facc44 100644 --- a/zero.Core/Mails/MailTemplateValidator.cs +++ b/zero.Core/Mails/MailTemplateValidator.cs @@ -4,10 +4,10 @@ namespace zero.Mails; public class MailTemplateValidator : ZeroValidator { - public MailTemplateValidator(ZeroValidationContext ctx, IMailDispatcher dispatcher) + public MailTemplateValidator(IZeroStore store, IMailDispatcher dispatcher = null) { RuleFor(x => x.Name).NotEmpty().Length(2, 80); - RuleFor(x => x.Key).NotEmpty().Unique(ctx); + RuleFor(x => x.Key).NotEmpty().Unique(store); RuleFor(x => x.Subject).NotEmpty().Length(2, 120); RuleFor(x => x.Preheader).MaximumLength(240); diff --git a/zero.Core/Mails/MailTemplatesStore.cs b/zero.Core/Mails/MailTemplatesStore.cs index 2aab401d..ec902a55 100644 --- a/zero.Core/Mails/MailTemplatesStore.cs +++ b/zero.Core/Mails/MailTemplatesStore.cs @@ -1,17 +1,10 @@ -using FluentValidation.Results; -using Raven.Client.Documents; +using Raven.Client.Documents; namespace zero.Mails; public class MailTemplatesStore : EntityStore, IMailTemplatesStore { - protected IMailDispatcher Dispatcher { get; set; } - - - public MailTemplatesStore(IStoreContext context, IMailDispatcher dispatcher = null) : base(context) - { - Dispatcher = dispatcher; - } + public MailTemplatesStore(IStoreContext context) : base(context) { } /// @@ -19,13 +12,6 @@ public class MailTemplatesStore : EntityStore, IMailTemplatesStore { return await Session.Query().FirstOrDefaultAsync(x => x.Key == key); } - - - /// - public override Task Validate(ZeroValidationContext ctx, MailTemplate model) - { - return new MailTemplateValidator(ctx, Dispatcher).ValidateAsync(model); - } } diff --git a/zero.Core/Stores/EntityStore.cs b/zero.Core/Stores/EntityStore.cs index 4d475cb1..89a90ef1 100644 --- a/zero.Core/Stores/EntityStore.cs +++ b/zero.Core/Stores/EntityStore.cs @@ -25,12 +25,12 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, I protected IZeroOptions Options { get; private set; } - public EntityStore(IStoreContext collectionContext) + public EntityStore(IStoreContext ctx) { - Context = collectionContext.Context; - Interceptors = collectionContext.Interceptors; - Options = collectionContext.Options; - Operations = new StoreOperations(collectionContext); + Context = ctx.Context; + Interceptors = ctx.Interceptors; + Options = ctx.Options; + Operations = new StoreOperations(ctx); } @@ -62,10 +62,10 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, I public virtual string GetChangeToken(T model) => Operations.GetChangeToken(model); /// - public virtual Task> Create(T model) => Operations.Create(model, async (m, ctx) => await Validate(ctx, m)); + public virtual Task> Create(T model) => Operations.Create(model, async m => await Validate(m)); /// - public virtual Task> Update(T model) => Operations.Update(model, async (m, ctx) => await Validate(ctx, m)); + public virtual Task> Update(T model) => Operations.Update(model, async m => await Validate(m)); /// public virtual Task>> Sort(string[] sortedIds) => Operations.Sort(sortedIds); @@ -74,28 +74,14 @@ public abstract class EntityStore : IEntityStore where T : ZeroIdEntity, I public virtual Task> Delete(T model) => Operations.Delete(model); /// - public virtual async Task Validate(ZeroValidationContext ctx, T model) - { - ZeroValidator validator = new(); - ValidationRules(validator); - return await validator.ValidateAsync(model); - } - - /// - public Task Validate(T model) => Validate(new ZeroValidationContext() - { - Context = Context, - Session = Session, - Operation = ValidationOp.Unknown - }, model); - + public virtual Task Validate(T model) => Operations.Validate(model); /// /// Create rules for validation /// + [Obsolete] protected virtual void ValidationRules(ZeroValidator validator) { } - /// /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() /// @@ -170,7 +156,7 @@ public interface IEntityStore where T : ZeroIdEntity, ISupportsFlavors, ISupp /// /// Validates an entity in this collection /// - Task Validate(ZeroValidationContext ctx, T model); + Task Validate(T model); /// /// Creates an entity with an optional validator diff --git a/zero.Core/Stores/StoreContext.cs b/zero.Core/Stores/StoreContext.cs index 5ee575d7..b8182ac6 100644 --- a/zero.Core/Stores/StoreContext.cs +++ b/zero.Core/Stores/StoreContext.cs @@ -12,14 +12,17 @@ public class StoreContext : IStoreContext public IStoreCache Cache { get; private set; } + public IServiceProvider Services { get; private set; } - public StoreContext(IZeroContext context, IInterceptors interceptors, IStoreCache cache) + + public StoreContext(IZeroContext context, IInterceptors interceptors, IStoreCache cache, IServiceProvider serviceProvider) { Store = context.Store; Options = context.Options; Context = context; Interceptors = interceptors; Cache = cache; + Services = serviceProvider; } } @@ -35,4 +38,6 @@ public interface IStoreContext IInterceptors Interceptors { get; } IStoreCache Cache { get; } + + IServiceProvider Services { get; } } \ No newline at end of file diff --git a/zero.Core/Stores/StoreOperations.Write.cs b/zero.Core/Stores/StoreOperations.Write.cs index 9adcc3d5..bf864eff 100644 --- a/zero.Core/Stores/StoreOperations.Write.cs +++ b/zero.Core/Stores/StoreOperations.Write.cs @@ -5,13 +5,13 @@ namespace zero.Stores; public partial class StoreOperations : IStoreOperations { /// - public virtual Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); + public virtual Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); /// - public virtual Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); + public virtual Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new() => Save(model, validate); /// - protected virtual async Task> Save(T model, Func> validate = null) where T : ZeroIdEntity, new() + protected virtual async Task> Save(T model, Func> validate = null) where T : ZeroIdEntity, new() { if (model == null) { @@ -52,12 +52,7 @@ public partial class StoreOperations : IStoreOperations // run validator if (validate != null) { - ValidationResult validation = await validate(model, new ZeroValidationContext() - { - Context = Context, - Session = Session, - Operation = isUpdate ? ValidationOp.Update : ValidationOp.Create - }); + ValidationResult validation = await validate(model); if (!validation.IsValid) { diff --git a/zero.Core/Stores/StoreOperations.cs b/zero.Core/Stores/StoreOperations.cs index 410ac123..2906231f 100644 --- a/zero.Core/Stores/StoreOperations.cs +++ b/zero.Core/Stores/StoreOperations.cs @@ -1,4 +1,5 @@ using FluentValidation.Results; +using Microsoft.Extensions.DependencyInjection; using Raven.Client.Documents.Indexes; using Raven.Client.Documents.Linq; using System.Security.Claims; @@ -25,6 +26,8 @@ public partial class StoreOperations : protected FlavorOptions Flavors { get; private set; } + protected IServiceProvider Services { get; private set; } + string _overrideDatabase = null; @@ -32,6 +35,7 @@ public partial class StoreOperations : { Context = context.Context; Interceptors = context.Interceptors; + Services = context.Services; Flavors = context.Options.For(); Config = config ?? new(); } @@ -94,6 +98,22 @@ public partial class StoreOperations : } + /// + /// Validates an entity + /// + public async Task Validate(T model) where T : ZeroIdEntity, new() + { + IZeroMergedValidator validator = Services.GetService>(); + + if (validator == null) + { + return new(); + } + + return await validator.ValidateAsync(model); + } + + /// /// Do only return the model when it is set to active or inactive entities are included with IncludeInactive() /// @@ -186,15 +206,20 @@ public interface IStoreOperations /// string GetChangeToken(T model) where T : ZeroIdEntity, new(); + /// + /// Validates an entity + /// + Task Validate(T model) where T : ZeroIdEntity, new(); + /// /// Creates an entity with an optional validator /// - Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new(); + Task> Create(T model, Func> validate = null) where T : ZeroIdEntity, new(); /// /// Updates an entity with an optional validator /// - Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new(); + Task> Update(T model, Func> validate = null) where T : ZeroIdEntity, new(); /// Task>> Sort(string[] sortedIds) where T : ZeroIdEntity, ISupportsSorting, new(); diff --git a/zero.Core/Validation/ValidatorExtensions.cs b/zero.Core/Validation/ValidatorExtensions.cs index 3903cb35..ab898157 100644 --- a/zero.Core/Validation/ValidatorExtensions.cs +++ b/zero.Core/Validation/ValidatorExtensions.cs @@ -121,11 +121,11 @@ public static class ValidatorExtensions /// /// Check if this value is unique within a collection /// - public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity + public static IRuleBuilderOptions Unique(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity { return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => { - bool any = await ctx.Session.Advanced.AsyncDocumentQuery() + bool any = await store.Session().Advanced.AsyncDocumentQuery() .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) .WhereEquals(context.PropertyName.ToPascalCaseId(), value) .AnyAsync(cancellation); @@ -138,11 +138,11 @@ public static class ValidatorExtensions /// /// Check if this value is at least set once to the expected value within a collection /// - public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx, TProperty expectedValue) where T : ZeroEntity + public static IRuleBuilderOptions ExpectAnyUnique(this IRuleBuilder ruleBuilder, IZeroStore store, TProperty expectedValue) where T : ZeroEntity { return ruleBuilder.MustAsync(async (entity, value, context, cancellation) => { - return await ctx.Session.Advanced.AsyncDocumentQuery() + return await store.Session().Advanced.AsyncDocumentQuery() .WhereNotEquals(nameof(ZeroIdEntity.Id), entity.Id) .WhereEquals(context.PropertyName.ToPascalCaseId(), expectedValue) .AnyAsync(cancellation); @@ -153,16 +153,16 @@ public static class ValidatorExtensions /// /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity { - return ruleBuilder.Exists(ctx); + return ruleBuilder.Exists(store); } /// /// Check if this reference exists and is an entity which can be referenced (appId = shared for shareable entities or appId = current) /// - public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, ZeroValidationContext ctx) where T : ZeroEntity where TReference : ZeroEntity + public static IRuleBuilderOptions Exists(this IRuleBuilder ruleBuilder, IZeroStore store) where T : ZeroEntity where TReference : ZeroEntity { return ruleBuilder.MustAsync(async (entity, id, context, cancellation) => { @@ -171,7 +171,7 @@ public static class ValidatorExtensions return true; } - return await ctx.Session.Query().AnyAsync(x => x.Id == id); + return await store.Session().Query().AnyAsync(x => x.Id == id); }).WithMessage("@errors.forms.reference_notfound"); } } diff --git a/zero.Core/Validation/ZeroMergedValidator.cs b/zero.Core/Validation/ZeroMergedValidator.cs new file mode 100644 index 00000000..5d3a5ac6 --- /dev/null +++ b/zero.Core/Validation/ZeroMergedValidator.cs @@ -0,0 +1,98 @@ +using FluentValidation; +using FluentValidation.Results; +using System.Collections.Concurrent; + +namespace zero.Validation; + +public class ZeroMergedValidator : IZeroMergedValidator +{ + protected IEnumerable> Validators { get; private set; } + + ConcurrentDictionary>> TypeCache = new(); + + + public ZeroMergedValidator(IEnumerable> validators) + { + Validators = validators; + } + + + /// + public async Task ValidateAsync(T model) + { + ValidationResult result = new(); + + foreach (IValidator validator in ResolveFor(model)) + { + ValidationResult innerResult = await validator.ValidateAsync(model); + result.Errors.AddRange(innerResult.Errors); + } + + return result; + } + + + /// + public IEnumerable> ResolveFor(T model) + { + Type type = typeof(T); + + if (!TypeCache.TryGetValue(type, out IEnumerable> validators)) + { + validators = Validators.Where(validator => CanHandle(validator, type)).ToList(); + TypeCache.TryAdd(type, validators); + } + + return validators; + } + + + /// + /// Checks whether a certain validator can handle the give type + /// + bool CanHandle(IValidator validator, Type modelType) + { + Type validatorType = validator.GetType(); + Type typeToFind = typeof(ZeroValidator<,>); + + Type findValidatorBase(Type type) + { + if (type.BaseType == null) + { + return null; + } + + if (type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeToFind) + { + return type.BaseType; + } + + return findValidatorBase(type.BaseType); + } + + Type zeroValidatorType = findValidatorBase(validatorType); + + if (zeroValidatorType == null) + { + return false; + } + + Type implementationType = zeroValidatorType.GenericTypeArguments[1]; + + return implementationType.IsAssignableFrom(modelType); + } +} + + +public interface IZeroMergedValidator +{ + /// + /// Get all validators which can run for the given model + /// + IEnumerable> ResolveFor(T model); + + /// + /// Validates a model by using all registered ZeroValidators for this entity type + /// + Task ValidateAsync(T model); +} \ No newline at end of file diff --git a/zero.Core/Validation/ZeroValidationContext.cs b/zero.Core/Validation/ZeroValidationContext.cs deleted file mode 100644 index 30f1d17d..00000000 --- a/zero.Core/Validation/ZeroValidationContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace zero.Validation; - -public class ZeroValidationContext -{ - public IZeroContext Context { get; set; } - - public IZeroDocumentSession Session { get; set; } - - public ValidationOp Operation { get; set; } -} - -public enum ValidationOp -{ - Unknown = 0, - Create = 1, - Update = 2 -} \ No newline at end of file diff --git a/zero.Core/Validation/ZeroValidationModule.cs b/zero.Core/Validation/ZeroValidationModule.cs new file mode 100644 index 00000000..a33d66cd --- /dev/null +++ b/zero.Core/Validation/ZeroValidationModule.cs @@ -0,0 +1,12 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace zero.Validation; + +internal class ZeroValidationModule : ZeroModule +{ + public override void ConfigureServices(IServiceCollection services, IConfiguration configuration) + { + services.AddScoped(typeof(IZeroMergedValidator<>), typeof(ZeroMergedValidator<>)); + } +} \ No newline at end of file diff --git a/zero.Core/Validation/ZeroValidator.cs b/zero.Core/Validation/ZeroValidator.cs index 837f54f0..acdcf9e3 100644 --- a/zero.Core/Validation/ZeroValidator.cs +++ b/zero.Core/Validation/ZeroValidator.cs @@ -26,4 +26,4 @@ public abstract class ZeroValidator : AbstractValid } return base.ValidateAsync((TImplementation)instance, cancellation); } -} +} \ No newline at end of file diff --git a/zero.Core/ZeroBuilder.cs b/zero.Core/ZeroBuilder.cs index 7ad1873e..1892ecba 100644 --- a/zero.Core/ZeroBuilder.cs +++ b/zero.Core/ZeroBuilder.cs @@ -42,6 +42,7 @@ public class ZeroBuilder Modules.Add(); Modules.Add(); Modules.Add(); + Modules.Add(); Modules.Add(); Modules.Add(); Modules.Add();