Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement validation for extra properties of the extensible objects #3561

Merged
merged 8 commits into from
Apr 11, 2020
112 changes: 105 additions & 7 deletions docs/en/Object-Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,25 +156,123 @@ ObjectExtensionManager.Instance
);
````

#### Property Configuration
### Property Configuration

`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition.

Example:
`AddOrUpdateProperty` can also get an action that can perform additional configuration on the property definition:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.CheckPairDefinitionOnMapping = false;
//Configure options...
});
````

> `options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document.

The following sections explain the fundamental property configuration options.

#### CheckPairDefinitionOnMapping

Controls how to check property definitions while mapping two extensible objects. See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option better.

## Validation

You may want to add some **validation rules** for the extra properties you've defined. `AddOrUpdateProperty` method options allows two ways of performing validation:

1. You can add **data annotation attributes** for a property.
2. You can write an action (code block) to perform a **custom validation**.

Validation works when you use the object in a method that is **automatically validated** (e.g. controller actions, page handler methods, application service methods...). So, all extra properties are validated whenever the extended object is being validated.

### Data Annotation Attributes

All of the standard data annotation attributes are valid for extra properties. Example:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserCreateDto, string>(
"SocialSecurityNumber",
options =>
{
options.ValidationAttributes.Add(new RequiredAttribute());
options.ValidationAttributes.Add(
new StringLengthAttribute(32) {
MinimumLength = 6
}
);
});
````

With this configuration, `IdentityUserCreateDto` objects will be invalid without a valid `SocialSecurityNumber` value provided.

### Custom Validation

If you need, you can add a custom action that is executed to validate the extra properties. Example:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUserCreateDto, string>(
"SocialSecurityNumber",
options =>
{
options.Validators.Add(context =>
{
var socialSecurityNumber = context.Value as string;

if (socialSecurityNumber == null ||
socialSecurityNumber.StartsWith("X"))
{
context.ValidationErrors.Add(
new ValidationResult(
"Invalid social security number: " + socialSecurityNumber,
new[] { "SocialSecurityNumber" }
)
);
}
});
});
````

> See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option.
`context.ServiceProvider` can be used to resolve a service dependency for advanced scenarios.

In addition to add custom validation logic for a single property, you can add a custom validation logic that is executed in object level. Example:

`options` has a dictionary, named `Configuration` which makes the object extension definitions even extensible. It is used by the EF Core to map extra properties to table fields in the database. See the [extending entities](Customizing-Application-Modules-Extending-Entities.md) document.
````csharp
ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUserCreateDto>(objConfig =>
{
//Define two properties with their own validation rules

objConfig.AddOrUpdateProperty<string>("Password", propertyConfig =>
{
propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
});

objConfig.AddOrUpdateProperty<string>("PasswordRepeat", propertyConfig =>
{
propertyConfig.ValidationAttributes.Add(new RequiredAttribute());
});

//Write a common validation logic works on multiple properties

objConfig.Validators.Add(context =>
{
if (context.ValidatingObject.GetProperty<string>("Password") !=
context.ValidatingObject.GetProperty<string>("PasswordRepeat"))
{
context.ValidationErrors.Add(
new ValidationResult(
"Please repeat the same password!",
new[] { "Password", "PasswordRepeat" }
)
);
}
});
});
````

## Object to Object Mapping

Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Auditing;
using Volo.Abp.Data;
using Volo.Abp.ObjectExtending;

namespace Volo.Abp.Domain.Entities
{
Expand Down Expand Up @@ -56,6 +58,14 @@ public virtual void ClearDistributedEvents()
{
_distributedEvents.Clear();
}

public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return ExtensibleObjectValidator.GetValidationErrors(
this,
validationContext
);
}
}

[Serializable]
Expand Down Expand Up @@ -115,5 +125,13 @@ public virtual void ClearDistributedEvents()
{
_distributedEvents.Clear();
}

public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return ExtensibleObjectValidator.GetValidationErrors(
this,
validationContext
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,27 @@

namespace Volo.Abp.Data
{
//TODO: Move to Volo.Abp.Data.ObjectExtending namespace at v3.0

public static class HasExtraPropertiesExtensions
{
public static bool HasProperty(this IHasExtraProperties source, string name)
{
return source.ExtraProperties.ContainsKey(name);
}

public static object GetProperty(this IHasExtraProperties source, string name)
public static object GetProperty(this IHasExtraProperties source, string name, object defaultValue = null)
{
return source.ExtraProperties?.GetOrDefault(name);
return source.ExtraProperties?.GetOrDefault(name)
?? defaultValue;
}

public static TProperty GetProperty<TProperty>(this IHasExtraProperties source, string name)
public static TProperty GetProperty<TProperty>(this IHasExtraProperties source, string name, TProperty defaultValue = default)
{
var value = source.GetProperty(name);
if (value == default)
if (value == null)
{
return default;
return defaultValue;
}

if (TypeHelper.IsPrimitiveExtended(typeof(TProperty), includeEnums: true))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

namespace Volo.Abp.Data
{
//TODO: Move to Volo.Abp.Data.ObjectExtending namespace at v3.0

public interface IHasExtraProperties
{
Dictionary<string, object> ExtraProperties { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using Volo.Abp.Data;

namespace Volo.Abp.ObjectExtending
{
[Serializable]
public class ExtensibleObject : IHasExtraProperties
public class ExtensibleObject : IHasExtraProperties, IValidatableObject
{
public Dictionary<string, object> ExtraProperties { get; protected set; }

public ExtensibleObject()
{
ExtraProperties = new Dictionary<string, object>();
}

public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
return ExtensibleObjectValidator.GetValidationErrors(
this,
validationContext
);
}
}
}
Loading