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

Extensible object manager docs #3546

Merged
merged 7 commits into from
Apr 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions docs/en/AutoMapper-Integration.md

This file was deleted.

26 changes: 17 additions & 9 deletions docs/en/Best-Practices/Application-Services.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@

##### Basic DTO

**Do** define a **basic** DTO for an entity.
**Do** define a **basic** DTO for an aggregate root.

- Include all the **primitive properties** directly on the entity.
- Exception: Can **exclude** properties for **security** reasons (like User.Password).
- Include all the **primitive properties** directly on the aggregate root.
- Exception: Can **exclude** properties for **security** reasons (like `User.Password`).
- Include all the **sub collections** of the entity where every item in the collection is a simple **relation DTO**.
- Inherit from one of the **extensible entity DTO** classes for aggregate roots (and entities implement the `IHasExtraProperties`).

Example:

```c#
[Serializable]
public class IssueDto : FullAuditedEntityDto<Guid>
public class IssueDto : ExtensibleFullAuditedEntityDto<Guid>
{
public string Title { get; set; }
public string Text { get; set; }
Expand Down Expand Up @@ -57,7 +58,7 @@ Example:

````C#
[Serializable]
public class IssueWithDetailsDto : FullAuditedEntityDto<Guid>
public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto<Guid>
{
public string Title { get; set; }
public string Text { get; set; }
Expand All @@ -66,14 +67,14 @@ public class IssueWithDetailsDto : FullAuditedEntityDto<Guid>
}

[Serializable]
public class MilestoneDto : EntityDto<Guid>
public class MilestoneDto : ExtensibleEntityDto<Guid>
{
public string Name { get; set; }
public bool IsClosed { get; set; }
}

[Serializable]
public class LabelDto : EntityDto<Guid>
public class LabelDto : ExtensibleEntityDto<Guid>
{
public string Name { get; set; }
public string Color { get; set; }
Expand Down Expand Up @@ -120,6 +121,7 @@ Task<List<QuestionWithDetailsDto>> GetListAsync(QuestionListQueryDto queryDto);

* **Do** use the `CreateAsync` **method name**.
* **Do** get a **specialized input** DTO to create the entity.
* **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed.
* **Do** use **data annotations** for input validation.
* Share constants between domain wherever possible (via constants defined in the **domain shared** package).
* **Do** return **the detailed** DTO for new created entity.
Expand All @@ -135,10 +137,11 @@ The related **DTO**:

````C#
[Serializable]
public class CreateQuestionDto
public class CreateQuestionDto : ExtensibleObject
{
[Required]
[StringLength(QuestionConsts.MaxTitleLength, MinimumLength = QuestionConsts.MinTitleLength)]
[StringLength(QuestionConsts.MaxTitleLength,
MinimumLength = QuestionConsts.MinTitleLength)]
public string Title { get; set; }

[StringLength(QuestionConsts.MaxTextLength)]
Expand All @@ -152,6 +155,7 @@ public class CreateQuestionDto

- **Do** use the `UpdateAsync` **method name**.
- **Do** get a **specialized input** DTO to update the entity.
- **Do** inherit the DTO class from the `ExtensibleObject` (or any other class implements the `IHasExtraProperties`) to allow to pass extra properties if needed.
- **Do** get the Id of the entity as a separated primitive parameter. Do not include to the update DTO.
- **Do** use **data annotations** for input validation.
- Share constants between domain wherever possible (via constants defined in the **domain shared** package).
Expand Down Expand Up @@ -200,6 +204,10 @@ This method votes a question and returns the current score of the question.

* **Do not** use LINQ/SQL for querying data from database inside the application service methods. It's repository's responsibility to perform LINQ/SQL queries from the data source.

#### Extra Properties

* **Do** use either `MapExtraPropertiesTo` extension method ([see](Object-Extensions.md)) or configure the object mapper (`MapExtraProperties`) to allow application developers to be able to extend the objects and services.

#### Manipulating / Deleting Entities

* **Do** always get all the related entities from repositories to perform the operations on them.
Expand Down
1 change: 1 addition & 0 deletions docs/en/Best-Practices/Data-Transfer-Objects.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* **Do** define DTOs in the **application contracts** package.
* **Do** inherit from the pre-built **base DTO classes** where possible and necessary (like `EntityDto<TKey>`, `CreationAuditedEntityDto<TKey>`, `AuditedEntityDto<TKey>`, `FullAuditedEntityDto<TKey>` and so on).
* **Do** inherit from the **extensible DTO** classes for the **aggregate roots** (like `ExtensibleAuditedEntityDto<TKey>`), because aggregate roots are extensible objects and extra properties are mapped to DTOs in this way.
* **Do** define DTO members with **public getter and setter**.
* **Do** use **data annotations** for **validation** on the properties of DTOs those are inputs of the service.
* **Do** not add any **logic** into DTOs except implementing `IValidatableObject` when necessary.
Expand Down
266 changes: 265 additions & 1 deletion docs/en/Object-Extensions.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,267 @@
# Object Extensions

TODO
ABP Framework provides an **object extension system** to allow you to **add extra properties** to an existing object **without modifying** the related class. This allows to extend functionalities implemented by a depended [application module](Modules/Index.md), especially when you want to [extend entities](Customizing-Application-Modules-Extending-Entities.md) and [DTOs](Customizing-Application-Modules-Overriding-Services.md) defined by the module.

> Object extension system is not normally not needed for your own objects since you can easily add regular properties to your own classes.

## IHasExtraProperties Interface

This is the interface to make a class extensible. It simply defines a `Dictionary` property:

````csharp
Dictionary<string, object> ExtraProperties { get; }
````

Then you can add or get extra properties using this dictionary.

### Base Classes

`IHasExtraProperties` interface is implemented by several base classes by default:

* Implemented by the `AggregateRoot` class (see [entities](Entities.md)).
* Implemented by `ExtensibleEntityDto`, `ExtensibleAuditedEntityDto`... base [DTO](Data-Transfer-Objects.md) classes.
* Implemented by the `ExtensibleObject`, which is a simple base class can be inherited for any type of object.

So, if you inherit from these classes, your class will also be extensible. If not, you can always implement it manually.

### Fundamental Extension Methods

While you can directly use the `ExtraProperties` property of a class, it is suggested to use the following extension methods while working with the extra properties.

#### SetProperty

Used to set the value of an extra property:

````csharp
user.SetProperty("Title", "My Title");
user.SetProperty("IsSuperUser", true);
````

`SetProperty` returns the same object, so you can chain it:

````csharp
user.SetProperty("Title", "My Title")
.SetProperty("IsSuperUser", true);
````

#### GetProperty

Used to read the value of an extra property:

````csharp
var title = user.GetProperty<string>("Title");

if (user.GetProperty<bool>("IsSuperUser"))
{
//...
}
````

* `GetProperty` is a generic method and takes the object type as the generic parameter.
* Returns the default value if given property was not set before (default value is `0` for `int`, `false` for `bool`... etc).

##### Non Primitive Property Types

If your property type is not a primitive (int, bool, enum, string... etc) type, then you need to use non-generic version of the `GetProperty` which returns an `object`.

#### HasProperty

Used to check if the object has a property set before.

#### RemoveProperty

Used to remove a property from the object. Use this methods instead of setting a `null` value for the property.

### Some Best Practices

Using magic strings for the property names is dangerous since you can easily type the property name wrong - it is not type safe. Instead;

* Define a constant for your extra property names
* Create extension methods to easily set your extra properties.

Example:

````csharp
public static class IdentityUserExtensions
{
private const string TitlePropertyName = "Title";

public static void SetTitle(this IdentityUser user, string title)
{
user.SetProperty(TitlePropertyName, title);
}

public static string GetTitle(this IdentityUser user)
{
return user.GetProperty<string>(TitlePropertyName);
}
}
````

Then you can easily set or get the `Title` property:

````csharp
user.SetTitle("My Title");
var title = user.GetTitle();
````

## Object Extension Manager

While you can set arbitrary properties to an extensible object (which implements the `IHasExtraProperties` interface), `ObjectExtensionManager` is used to explicitly define extra properties for extensible classes.

Explicitly defining an extra property has some use cases:

* Allows to control how the extra property is handled on object to object mapping (see the section below).
* Allows to define metadata for the property. For example, you can map an extra property to a table field in the database while using the [EF Core](Entity-Framework-Core.md).

> `ObjectExtensionManager` implements the singleton pattern (`ObjectExtensionManager.Instance`) and you should define object extensions before your application startup. The [application startup template](Startup-Templates/Application.md) has some pre-defined static classes to safely define object extensions inside.

### AddOrUpdate

`AddOrUpdate` is the main method to define a extra properties or update extra properties for an object.

Example: Define extra properties for the `IdentityUser` entity:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdate<IdentityUser>(options =>
{
options.AddOrUpdateProperty<string>("SocialSecurityNumber");
options.AddOrUpdateProperty<bool>("IsSuperUser");
}
);
````

### AddOrUpdateProperty

While `AddOrUpdateProperty` can be used on the `options` as shown before, if you want to define a single extra property, you can use the shortcut extension method too:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>("SocialSecurityNumber");
````

Sometimes it would be practical to define a single extra property to multiple types. Instead of defining one by one, you can use the following code:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<string>(
new[]
{
typeof(IdentityUserDto),
typeof(IdentityUserCreateDto),
typeof(IdentityUserUpdateDto)
},
"SocialSecurityNumber"
);
````

#### Property Configuration

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

Example:

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

> See the "Object to Object Mapping" section to understand the `CheckPairDefinitionOnMapping` option.

`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.

## Object to Object Mapping

Assume that you've added an extra property to an extensible entity object and used auto [object to object mapping](Object-To-Object-Mapping.md) to map this entity to an extensible DTO class. You need to be careful in such a case, because the extra property may contain a **sensitive data** that should not be available to clients.

This section offers some **good practices** to control your extra properties on object mapping.

### MapExtraPropertiesTo

`MapExtraPropertiesTo` is an extension method provided by the ABP Framework to copy extra properties from an object to another in a controlled manner. Example usage:

````csharp
identityUser.MapExtraPropertiesTo(identityUserDto);
````

`MapExtraPropertiesTo` **requires to define properties** (as described above) in **both sides** (`IdentityUser` and `IdentityUserDto` in this case) in order to copy the value to the target object. Otherwise, it doesn't copy the value even if it does exists in the source object (`identityUser` in this example). There are some ways to overload this restriction.

#### MappingPropertyDefinitionChecks

`MapExtraPropertiesTo` gets an additional parameter to control the definition check for a single mapping operation:

````csharp
identityUser.MapExtraPropertiesTo(
identityUserDto,
MappingPropertyDefinitionChecks.None
);
````

> Be careful since `MappingPropertyDefinitionChecks.None` copies all extra properties without any check. `MappingPropertyDefinitionChecks` enum has other members too.

If you want to completely disable definition check for a property, you can do it while defining the extra property (or update an existing definition) as shown below:

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

#### Ignored Properties

You may want to ignore some properties on a specific mapping operation:

````csharp
identityUser.MapExtraPropertiesTo(
identityUserDto,
ignoredProperties: new[] {"MySensitiveProp"}
);
````

Ignored properties are not copied to the target object.

#### AutoMapper Integration

If you're using the [AutoMapper](https://automapper.org/) library, the ABP Framework also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above.

You can use the `MapExtraProperties()` method inside your mapping profile.

````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<IdentityUser, IdentityUserDto>()
.MapExtraProperties();
}
}
````

It has the same parameters with the `MapExtraPropertiesTo` method.

## Entity Framework Core Database Mapping

If you're using the EF Core, you can map an extra property to a table field in the database. Example:

````csharp
ObjectExtensionManager.Instance
.AddOrUpdateProperty<IdentityUser, string>(
"SocialSecurityNumber",
options =>
{
options.MapEfCore(b => b.HasMaxLength(32));
}
);
````

See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more.
17 changes: 17 additions & 0 deletions docs/en/Object-To-Object-Mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,23 @@ options.AddProfile<MyProfile>(validate: true);

> If you have multiple profiles and need to enable validation only for a few of them, first use `AddMaps` without validation, then use `AddProfile` for each profile you want to validate.

### Mapping the Object Extensions

[Object extension system](Object-Extensions.md) allows to define extra properties for existing classes. ABP Framework provides a mapping definition extension to properly map extra properties of two objects.

````csharp
public class MyProfile : Profile
{
public MyProfile()
{
CreateMap<User, UserDto>()
.MapExtraProperties();
}
}
````

It is suggested to use the `MapExtraProperties()` method if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](Object-Extensions.md) for more.

## Advanced Topics

### IObjectMapper<TContext> Interface
Expand Down
Loading