diff --git a/docs/en/AutoMapper-Integration.md b/docs/en/AutoMapper-Integration.md deleted file mode 100644 index d197861f250..00000000000 --- a/docs/en/AutoMapper-Integration.md +++ /dev/null @@ -1,3 +0,0 @@ -## AutoMapper Integration - -TODO \ No newline at end of file diff --git a/docs/en/Best-Practices/Application-Services.md b/docs/en/Best-Practices/Application-Services.md index 0979304931e..876105c2143 100644 --- a/docs/en/Best-Practices/Application-Services.md +++ b/docs/en/Best-Practices/Application-Services.md @@ -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 +public class IssueDto : ExtensibleFullAuditedEntityDto { public string Title { get; set; } public string Text { get; set; } @@ -57,7 +58,7 @@ Example: ````C# [Serializable] -public class IssueWithDetailsDto : FullAuditedEntityDto +public class IssueWithDetailsDto : ExtensibleFullAuditedEntityDto { public string Title { get; set; } public string Text { get; set; } @@ -66,14 +67,14 @@ public class IssueWithDetailsDto : FullAuditedEntityDto } [Serializable] -public class MilestoneDto : EntityDto +public class MilestoneDto : ExtensibleEntityDto { public string Name { get; set; } public bool IsClosed { get; set; } } [Serializable] -public class LabelDto : EntityDto +public class LabelDto : ExtensibleEntityDto { public string Name { get; set; } public string Color { get; set; } @@ -120,6 +121,7 @@ Task> 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. @@ -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)] @@ -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). @@ -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. diff --git a/docs/en/Best-Practices/Data-Transfer-Objects.md b/docs/en/Best-Practices/Data-Transfer-Objects.md index 0c8580abb7c..0fca0e86f2e 100644 --- a/docs/en/Best-Practices/Data-Transfer-Objects.md +++ b/docs/en/Best-Practices/Data-Transfer-Objects.md @@ -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`, `CreationAuditedEntityDto`, `AuditedEntityDto`, `FullAuditedEntityDto` and so on). + * **Do** inherit from the **extensible DTO** classes for the **aggregate roots** (like `ExtensibleAuditedEntityDto`), 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. diff --git a/docs/en/Object-Extensions.md b/docs/en/Object-Extensions.md index fad3ff2b0ce..bbcb96374c9 100644 --- a/docs/en/Object-Extensions.md +++ b/docs/en/Object-Extensions.md @@ -1,3 +1,267 @@ # Object Extensions -TODO \ No newline at end of file +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 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("Title"); + +if (user.GetProperty("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(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(options => + { + options.AddOrUpdateProperty("SocialSecurityNumber"); + options.AddOrUpdateProperty("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("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( + 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( + "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( + "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() + .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( + "SocialSecurityNumber", + options => + { + options.MapEfCore(b => b.HasMaxLength(32)); + } + ); +```` + +See the [Entity Framework Core Integration document](Entity-Framework-Core.md) for more. \ No newline at end of file diff --git a/docs/en/Object-To-Object-Mapping.md b/docs/en/Object-To-Object-Mapping.md index 52260402f13..b7463607e9f 100644 --- a/docs/en/Object-To-Object-Mapping.md +++ b/docs/en/Object-To-Object-Mapping.md @@ -145,6 +145,23 @@ options.AddProfile(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() + .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 Interface diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 471e215e3a2..f45a4192bb6 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -155,6 +155,10 @@ { "text": "Data Filtering", "path": "Data-Filtering.md" + }, + { + "text": "Object Extensions", + "path": "Object-Extensions.md" } ] }, diff --git a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs similarity index 99% rename from framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs rename to framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs index f2b7aeecdbb..671aff40f21 100644 --- a/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtendedObjectMapper.cs +++ b/framework/src/Volo.Abp.ObjectExtending/Volo/Abp/ObjectExtending/ExtensibleObjectMapper.cs @@ -122,8 +122,6 @@ public static void MapExtraPropertiesTo( } } - //TODO: Move these methods to a class like ObjectExtensionHelper - public static bool CanMapProperty( [NotNull] string propertyName, MappingPropertyDefinitionChecks? definitionChecks = null,