Application Services¶
- Do create an application service for each aggregate root.
Application Service Interface¶
- Do define an
interface
for each application service in the application contracts package. - Do inherit from the
IApplicationService
interface. - Do use the
AppService
postfix for the interface name (ex:IProjectAppService
). - Do create DTOs (Data Transfer Objects) for inputs and outputs of the service.
- Do define DTOs based on the DTO best practices.
- Do order methods according to C / R / U / D
- Do create version folder each application service
- Do not get/return entities for the service methods.
Outputs¶
- Avoid to define too many output DTOs for same or related entities. Instead, define a basic and a detailed DTO for an entity
Basic DTO¶
Do define a basic DTO for an aggregate root.
- Include all the primitive properties directly on the aggregate root.
- Exception: Can exclude properties for security reasons (like
User.Password
).
- Exception: Can exclude properties for security reasons (like
- 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:
[Serializable]
public class ProjectDto : ExtensibleAuditedEntityDto<Guid>, IHasConcurrencyStamp
{
public string Name { get; set; }
public Guid BaseModelId { get; set; }
public Collection<ChangesDto> Changes { get; set; }
public string ConcurrencyStamp { get; set; }
}
[Serializable]
public class ChangesDto
{
public Guid ProjectId { get; set; }
public Guid ChangeId { get; set; }
}
Detailed DTO¶
-
Do define a detailed DTO for an entity if it has reference(s) to other aggregate roots.
-
Include all the primitive properties directly on the entity.
- Exception-1: Can exclude properties for security reasons (like
User.Password
). - Exception-2: Do exclude reference properties (like
BaseModelId
in the example above). Will already add details for the reference properties.
- Exception-1: Can exclude properties for security reasons (like
- Include a basic DTO property for every reference property.
- Include all the sub collections of the entity where every item in the collection is the basic DTO of the related entity.
[Serializable]
public class ProjectWithNavigationPropertiesDto : ExtensibleAuditedEntityDto<Guid>
{
public string Name { get; set; }
public BaseModelDto BaseModel { get; set; }
public Collection<ChangesDto> Changes { get; set; }
}
[Serializable]
public class ChangesDto
{
public Guid ProjectId { get; set; }
public Guid ChangeId { get; set; }
}
[Serializable]
public class BaseModelDto : EntityDto<Guid>, IHasConcurrencyStamp, IHasCreationTime
{
public string SchemaName { get; set; }
public string ServerName { get; set; }
public string ConcurrencyStamp { get; set; }
public DateTime CreationTime { get; set; }
}
Inputs¶
- Do use Input as both the parameter name and postfix for DTOs.
Task<ProjectDto> CreateAsync(CreateProjectInput input);
- Do not define any property in an input DTO that is not used in the service class.
- Do not share input DTOs between application service methods.
- Do not inherit an input DTO class from another one.
- May inherit from an abstract base DTO class and share some properties between different DTOs in that way. However, should be very careful in that case because manipulating the base DTO would effect all related DTOs and service methods. Avoid from that as a good practice.
Methods¶
- Do define service methods as asynchronous with Async postfix.
- Do not repeat the entity name in the method names.
- Example: Define
GetAsync(...)
instead ofGetProjectAsync(...)
in theIProjectAppService
.
- Example: Define
Creating A New Entity¶
- 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 theIHasExtraProperties
) 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.
- Do only require the minimum info to create the entity but provide possibility to set others as optional properties.
Example method:
Task<ProjectDto> CreateAsync(CreateProjectInput input);
The related DTO:
[Serializable]
public class CreateProjectInput : ExtensibleObject
{
public Guid? ExternalReferenceId { get; set; } = null;
[Required]
public string Name { get; set; }
public string Description { get; set; } = string.Empty;
[Required]
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public CreateProjectInput()
: base(setDefaultsForExtraProperties: false)
{
}
}
Getting A Single Entity¶
- Do use the
GetAsync
method name. - Do get Id with a primitive method parameter.
- Return the detailed DTO. Example:
WithDetails
indicates that the aggregate root has a child entity.
Task<ProjectWithDetailsDto> GetAsync(Guid id);
WithNavigationProperties
indicates that the aggregate root has a relation with another entity.
Task<ProjectWithNavigationPropertiesDto> GetWithNavigationPropertiesAsync(Guid id);
Getting A List Of Entities¶
- Do use the
GetListAsync
method name. - Do get a single DTO argument for filtering, sorting and paging if necessary.
- Do implement filters optional where possible.
- Do implement sorting & paging properties as optional and provide default values.
- Do limit maximum page size (for performance reasons).
- Do return a list of **detailed DTO**s. Example:
Task<PagedResultDto<ProjectWithDetailsDto>> GetListAsync(GetProjectInput input);
Getting A Lookup Values¶
- Do use the
Get{EntityName}LookupAsync
method name.- Do methods without parameter.
Task<ProjectDto> CreateAsync(CreateProjectInput input);
Task<ProjectWithDetailsDto> GetAsync(Guid id);
Task<List<UserDto>> GetUserLookupAsync();
Updating An Existing Entity¶
- 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 theIHasExtraProperties
) to allow to pass extra properties if needed. - Do implement
IHasConcurrencyStamp
for data - 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).
- Do return the detailed DTO for the updated entity.
- Do use to update bare minimum properties of the entity.
Example:
Task<ProjectDto> UpdateAsync(Guid id, UpdateProjectInput input);
Other Business Methods¶
- Can define additional methods to perform operations on the entity. Example:
Task<ProjectDto> ChangePriority(Guid id, ChangePriorityInput input);
This method changes priority property of the project and then returns ProjectDto
after change.
Deleting An Existing Entity¶
- Do use the
DeleteAsync
method name. - Do get Id with a primitive method parameter. Example:
Task DeleteAsync(Guid id);
Application Service Implementation¶
- Do develop the application layer completely independent from the web layer.
- Do implement application service interfaces in the application layer.
- Do use the naming convention. Ex: Create
ProductAppService
class for theIProductAppService
interface. - Do inherit from the
ApplicationService
base class.
- Do use the naming convention. Ex: Create
- Do make all public methods virtual, so developers may inherit and override them.
- Do not make private methods. Instead make them protected virtual, so developers may inherit and override them.
[Authorize(ProjectPlanningPermissions.Project.Permission)]
[RequiresFeature(ProjectPlanningFeatures.Enable)]
public class ProjectAppService :ProjectPlanningAppService, IProjectAppService
{
protected IProjectManager ProjectManager { get; }
protected IProjectRepository ProjectRepository { get; }
public ProjectAppService(
IProjectManager projectManager,
IProjectRepository projectRepository)
{
ProjectManager = projectManager;
ProjectRepository = projectRepository;
}
[Authorize(ProjectPlanningPermissions.Project.Create)]
public virtual async Task<ProjectDto> CreateAsync(CreateProjectInput input)
{
var project = await ProjectManager.CreateProjectAsync(
id: input.ExternalReferenceId.HasValue ? input.ExternalReferenceId : null,
name: input.Name,
description: input.Description,
baseModelId: input.BaseModelId,
priority: input.Priority,
startDate: input.StartDate
);
// Set definition id
project.SetDefinitionId(input.DefinitionId);
// Project creator will be always owner
project.SetOwnerId(CurrentUser.Id);
// Set dates
project.SetEndDate(input.EndDate);
// Set extra properties for project entity
foreach (var extraProperty in input.ExtraProperties)
{
project.ExtraProperties.Add(extraProperty.Key, extraProperty.Value);
}
// input.MapExtraPropertiesTo(project);
// Set external id as extra properties as well
if (input.ExternalReferenceId.HasValue)
project.SetExternalReference(id: input.ExternalReferenceId);
// Link projects
await ProjectManager.LinkSuccessorsAsync(
project: project,
successorIds: input.SuccessorIds
);
await ProjectRepository.InsertAsync(
entity: project,
autoSave: true
);
return ObjectMapper.Map<Project, ProjectDto>(project);
}
}
Using Repositories¶
- Do use the specifically designed repositories (like
IProductRepository
). - Do not use generic repositories (like
IRepository<Product>
).
Querying Data¶
- 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.
Manipulating / Deleting Entities¶
- Do always get all the related entities from repositories to perform the operations on them.
- Do call repository's Update/UpdateAsync method after updating an entity. Because, not all database APIs support change tracking & auto update.
Handle files¶
- Do not use any web components like
IFormFile
orStream
in the application services. If you want to serve a file you can usebyte[]
. - Do use a
Controller
to handle file uploading then pass thebyte[]
of the file to the application service method
Extra Properties¶
- Do use either
MapExtraPropertiesTo
extension method or configure the object mapper (MapExtraProperties
) to allow application developers to be able to extend the objects and services.
Using Other Application Services¶
-
Do not use other application services of the same module/application. Instead;
- Use domain layer to perform the required task.
- Extract a new class and share between the application services to accomplish the code reuse when necessary. But be careful to don't couple two use cases. They may seem similar at the beginning, but may evolve to different directions by time. So, use code sharing carefully.
-
Can use application services of others only if;
- They are parts of another module / microservice.
- The current module has only reference to the application contracts of the used module.