Domain Layer
The domain layer holds all your business logic. It is the one responsible for accessing/updating data, performing database operations, etc.
All services inherit from BaseService containing the following methods:
| Method | Signature | Description |
|---|---|---|
| ToEntity | TEntity ToEntity<TEntity>(object source) | Maps the source object to the entity type requested. |
| ToResponse | TResponse ToResponse<TResponse>(object source) | Maps the source object (entity) to the response type requested. |
| MapTo | TResponse MapTo<TResponse>(object source) | Maps the source object to the type requested. |
| GetDomainService | TDomainService GetDomainService<TDomainService>() | Gets a domain service. |
You can always add extra methods to your service.
Let's explore the available structures provided by the UpDevs SDK:
Base
Provides the basic structure for all domain services. It is the simplest form a domain service can take.
Enables the usage of IUnitOfWork.
Interface
public interface IMyDomainService : IDomainService
{
// ... Methods' signatures
}
Implementation
public class MyDomainService : BaseDomainService, IMyDomainService
{
public MyDomainService(IServiceProvider serviceProvider, IUnitOfWork unitOfWork)
: base(serviceProvider, unitOfWork) { }
// ... Methods
}
List
Inherits from BaseDomainService and adds extra functionality to implement the listing behavior. Requires the entity and response (this is the type of the model that represents the entity as it's returned to the outside - it can have a Response or Model suffix depending on your structure for this entity) types to be informed.
Methods Provided
They all have async options - ending with Async.
| Method | Signature | Description |
|---|---|---|
| List | PagedListModel<TResponse> List(SearchRequest? searchModel) | Returns the paged list of records using the specified filters. |
| GetByIds | TResponse[] GetByIds<TId>(TId[] ids) | Retrieves the records matching the provided identifiers. |
protected UpdateWithFilter | SearchRequest UpdateWithFilter(SearchRequest searchModel, ColumnFilter? filter = null) | Updates the search request to contain the new provided filter or the one that excludes the soft deleted records from the search, if a filter is not provided. |
protected GetSoftDeleteFilter | ColumnFilter GetSoftDeleteFilter() | Gets the filter to exclude soft deleted records from the search. |
protected GetByIdColumnFilter | ColumnFilter GetByIdColumnFilter(object value, object? operand = null) | Gets the filter to search records by their ID. |
protected GetOperandInForDataType | object? GetOperandInForDataType(DataType dataType) | Gets the proper IN operand according to the specified data type. |
protected GetSoftDeleteRequest | SearchRequest GetSoftDeleteRequest() | Gets a search request with a filter to exclude soft deleted records from the search. |
Interface
public interface IMyDomainService : IListDomainService<MyResponse>
{
// ... Other methods' signatures
}
Implementation
public class MyDomainService : BaseListDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(IServiceProvider serviceProvider, IUnitOfWork unitOfWork)
: base(serviceProvider, unitOfWork) { }
// ... Other methods
}
Customization
Here you can see all the fields allowing customization of the process and how to properly use them.
| Config | Description |
|---|---|
| Include | Relationships to be included while returning data from the list methods. If not set (default behavior), returns only the main entity. |
| ListSearchExpression | Allows to customize the text search. The field description (string) contains the text sent by the user, this text can be used to search in the fields and using the logic your application decided here. |
| ListExtraQuery | Allows to customize the text search. The search request can be modified in this function where you also have access to the request's session. |
| ListExcludedFilters | Names of the filters in the request that should be excluded from the search. |
| ListUseFindOnGetByIds | Whether a simple .Find() should be used for the GetByIds method (retrieve record). If true, no relationships will be loaded. |
| ListGetByIdIncludes | Relationships to be included while returning data from the GetById methods. If not set (default behavior), returns only the main entity. |
| ShouldSoftDelete | Whether the soft delete (logical removal) should be used instead of the physical one. |
| SoftDeletePropertyName | Name of the property used to identify the existence status of the record. Only requires setting if different than "IsDeleted". |
| IgnoreSoftDeletedInListGetByIds | Whether the records that were soft deleted should be ignored from the GetByIds call. |
| IgnoreSoftDeletedInList | Whether the records that were soft deleted should be ignored from the List call. |
| IdPropertyName | Name of the property used as identifier. Defaults to "Id". |
| IdDataType | Data type of the property used as identifier. Defaults to DataType.Guid |
Usage:
public class MyDomainService : BaseListDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(IServiceProvider serviceProvider, IUnitOfWork unitOfWork)
: base(serviceProvider, unitOfWork)
{
// Returns the Related relationship with the main MyEntity.
Include = myRecords => myRecords.Include(mr => mr.Related);
// Defines the operation to be performed while searching for a text (Description).
// Session data is also provided, should you need it.
ListSearchExpression = (description, session) =>
myEntity => myEntity.FullDescription.Contains(description);
// Filters that will be ignored by the default search.
// Useful when these filters are already being handled some place else,
// like ListExtraQuery.
ListExcludedFilters = new[] { "myIdArray" };
// Customizes the default search using the additional conditions provided here.
// Session data is also provided, should you need it.
ListExtraQuery = (searchRequest, session) =>
{
var relatedIds = searchRequest.Filters
.Where(filter => filter.Column == "myIdArray")
.Select(f => Guid.Parse(f.Value.ToString())).ToArray();
return entity => (
!relatedIds.Any()
|| entity.RelatedItems.Any(ri => relatedIds.Contains(ri.Id))
);
};
// If enabled, a simple .Find() will be used to retrieve a record.
// Increases performance, but does not load any related data.
ListUseFindOnGetByIds = true;
// Relationships to be included while retrieving a record by its ID.
ListGetByIdIncludes = myRecords => myRecords.Include(mr => mr.Related);
// If you're using Include and they are the same, you can simply:
ListGetByIdIncludes = Include;
// If you're using an identifier with a name other than "Id".
IdPropertyName = "MySuperDifferentId";
// If you're using a data type for your identifier different than Guid.
IdDataType = DataType.Number;
// Enables soft delete.
// If enabled, every time the Delete method is called for this entity,
// it'll update the property storing the removal status to true instead
// of phisically removing the record.
ShouldSoftDelete = true;
// If you're using a property to store the removal status with a name
// other than "IsDeleted".
SoftDeletePropertyName = "HasBeenRemoved";
// If false, returns even the records that were soft deleted in the
// GetByIds call.
IgnoreSoftDeletedInListGetByIds = false;
// If false, returns even the records that were soft deleted in the List call.
IgnoreSoftDeletedInList = false;
}
// ... Other methods
}
Search
Inherits from BaseListDomainService and adds extra functionality to implement the search behavior. Requires the entity and response (this is the type of the model that represents the entity as it's returned to the outside - it can have a Response or Model suffix depending on your structure for this entity) types to be informed.
Methods Provided
They all have async options - ending with Async.
Also includes the methods in BaseListDomainService.
| Method | Signature | Description |
|---|---|---|
| Search | PagedListModel<TResponse> Search(SearchRequest? searchModel) | Returns the paged list of records using the specified filters. |
Interface
public interface IMyDomainService : ISearchDomainService<MyResponse>
{
// ... Other methods' signatures
}
Implementation
public class MyDomainService : BaseSearchDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(IServiceProvider serviceProvider, IUnitOfWork unitOfWork)
: base(serviceProvider, unitOfWork) { }
// ... Other methods
}
Customization
Here you can see all the fields allowing customization of the process and how to properly use them.
| Config | Description |
|---|---|
| SearchExpression | Allows to customize the text search. The field description (string) contains the text sent by the user, this text can be used to search in the fields and using the logic your application decided here. |
| ExtraQuery | Allows to customize the search. The search request can be modified in this function where you also have access to the request's session. |
| ExcludedFilters | Names of the filters in the request that should be excluded from the search. |
| IgnoreSoftDeletedInSearch | Whether the records that were soft deleted should be ignored from the Search call. |
Usage:
public class MyDomainService : BaseSearchDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(IServiceProvider serviceProvider, IUnitOfWork unitOfWork)
: base(serviceProvider, unitOfWork)
{
// Defines the operation to be performed while searching for a text (Description).
// Session data is also provided, should you need it.
SearchExpression = (description, session) =>
myEntity => myEntity.FullDescription.Contains(description);
// Filters that will be ignored by the default search.
// Useful when these filters are already being handled some place else,
// like ExtraQuery.
ExcludedFilters = new[] { "myIdArray" };
// Customizes the default search using the additional conditions provided here.
// Session data is also provided, should you need it.
ExtraQuery = (searchRequest, session) =>
{
var relatedIds = searchRequest.Filters
.Where(filter => filter.Column == "myIdArray")
.Select(f => Guid.Parse(f.Value.ToString())).ToArray();
return entity => (
!relatedIds.Any()
|| entity.RelatedItems.Any(ri => relatedIds.Contains(ri.Id))
);
};
// If false, returns even the records that were soft deleted in the Search call.
IgnoreSoftDeletedInSearch = false;
// Setting up parent's properties.
ListSearchExpression = SearchExpression;
Include = myRecords => myRecords.Include(mr => mr.Related);
// ...
}
// ... Other methods
}
Entity
Inherits from BaseSearchDomainService and adds extra functionality to implement the CRUD behavior. Requires the entity, input (this is the type of the model that represents the entity as it's sent to service - it can have an Input or Model suffix depending on your structure for this entity) and response (this is the type of the model that represents the entity as it's returned to the outside - it can have a Response or Model suffix depending on your structure for this entity) types to be informed.
Methods Provided
They all have async options - ending with Async.
Also includes the methods in BaseSearchDomainService.
| Method | Signature | Description |
|---|---|---|
| Get | TResponse? Get(params object[] keyValues) | Retrieves an entity using its ID(s). |
| Get | TResponse? Get<TId>(TId id) | Retrieves an entity using its ID. |
| Create | TResponse Create(TInput data, bool shouldSave = true) | Inserts a new record in the database using the model provided by the input. |
| Create | TResponse Create(TInput data, IFormFileCollection files, bool shouldSave = true) | Inserts a new record in the database using the model provided by the input and files. |
| Update | TResponse Update(TInput data, bool shouldSave = true) | Updates the informed record in the database using the model provided by the input. |
| Update | TResponse Update(TInput data, IFormFileCollection files, bool shouldSave = true) | Updates the informed record in the database using the model provided by the input and files. |
| Delete | bool Delete(params object[] keyValues) | Removes a record from the database matching the composite primary key provided. |
| DeleteMany | bool DeleteMany<TId>(IEnumerable<TId> ids) | Removes records from the database matching the IDs provided. |
Interface
If you use the Model pattern instead of Input/Response for this entity, you only need to provide the model type once.
public interface IMyDomainService : IEntityDomainService<MyModel>
{
// ... Other methods' signatures
}
public interface IMyDomainService : IEntityDomainService<MyInput, MyResponse>
{
// ... Other methods' signatures
}
Implementation
If you use the Model pattern instead of Input/Response for this entity, you only need to provide the model type once.
public class MyDomainService : BaseEntityDomainService<MyEntity, MyModel>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource) { }
// ... Other methods
}
public class MyDomainService : BaseEntityDomainService<MyEntity, MyInput, MyResponse>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource) { }
// ... Other methods
}
Customization
Here you can see all the fields allowing customization of the process and how to properly use them.
| Config | Description |
|---|---|
| SetupEntityRelations | Configures the collections of the entity. Required when adding relationship items in collections. |
| GetByIdQuery | Function to customize the query used to retrieve a record by its ID. |
| HandleFiles | Handles the files provided, if any. Only required when working with files. Due to its async nature, HandleFiles can only be used in async Create/Update methods. |
| IgnoreSoftDeletedInGetById | Whether the records that were soft deleted should be ignored from the GetById call. |
| ShouldClearEntityCollections | Whether the entity collections should be cleared before invoking the transformations (SetupEntityRelations). |
| UseFindOnGet | Whether a simple .Find() should be used for the Get method (retrieve record). If true, no relationships will be loaded. |
| GetByIdIncludes | Customized includes to be loaded when calling GetById. |
Usage:
public class MyDomainService : BaseSearchDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource)
{
// Sets up the entity relations. Required when we make association in child
// collections.
SetupEntityRelations = (input, entity, isEdit) =>
{
foreach (var relItem in input.RelatedItems)
{
entity.RelatedItems.Add(ToEntity<RelatedItem>(relItem));
}
return entity;
};
// You can customize the GetById query, should you need it.
GetByIdQuery = id =>
{
return entity => entity.MyDifferentId == id;
};
// If you're working with files, here you can decide how they are handled.
HandleFiles = async (input, isEdit, files) =>
{
// Uploads the files, updates the input, etc.
// ...
return input;
};
// If false, returns even the records that were soft deleted in the GetById call.
IgnoreSoftDeletedInGetById = false;
// If true, the entity collections will be cleared before calling
// SetupEntityRelations.
ShouldClearEntityCollections = true;
// If enabled, a simple .Find() will be used to retrieve the record.
// Increases performance, but does not load any related data.
UseFindOnGet = true;
// Returns the Related relationship with the main MyEntity.
GetByIdIncludes = myRecords => myRecords.Include(mr => mr.Related);
// Setting up parent's properties.
ListSearchExpression = SearchExpression = (description, session) =>
myEntity => myEntity.FullDescription.Contains(description);
// ...
}
// ... Other methods
}
Handle Files
Even though already specified above, we'll get into a little more detail explaining how you can use the HandleFiles functionality.
We're going to show two options to handle files, using a Base64 property and using attached files. For all cases, the function has
the following parameters:
| Name | Type | Description |
|---|---|---|
input | Data type used for the input | Model with the input data provided by the user. |
isEdit | boolean | Whether the record is being created or updated. |
files | IFormFileCollection | If using attached files instead of Base64, they'll be provided here. |
Option 1: Using a string property from input as Base64
For this option, FileContent should not be a property in the entity, only in the input model. The exception would be if the Base64 file content were being saved in the database. But in that case, the code below would be unnecessary, you wouldn't need to handle files, since it's just a text property.
public class MyDomainService : BaseSearchDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource)
{
HandleFiles = async (input, isEdit, files) =>
{
var fileUrl = await SendBase64FileToAzure(input.FileName, input.FileContent);
input.FileUrl = fileUrl;
return input;
};
// ...
}
// ... Other methods
}
Option 2: Using attached files
public class MyDomainService : BaseSearchDomainService<MyEntity, MyResponse>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource)
{
HandleFiles = async (input, isEdit, files) =>
{
foreach (var file in files)
{
var fileContent = file.OpenReadStream().ToByteArray();
var fileUrl = await SendByteArrayFileToAzure(file.Name, fileContent);
input.FilesUrls += (input.FilesUrls.Length > 0 ? "\n" : "") + fileUrl;
}
return input;
};
// ...
}
// ... Other methods
}
Management Service
To increase productivity, the UpDevs SDK handles certain common tasks, like users reporting errors inside your application. To use this
functionality, you must create a new domain service and make it inherit from BaseManagementDomainService providing the data type of your
identifier. Here's an example:
public class ManagementDomainService : BaseManagementDomainService<Guid>
{
public ManagementDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork
) : base(serviceProvider, unitOfWork) { }
public override async void ReportError(Guid id, ExtraErrorInfo<Guid> data)
{
// id represents the identifier of the log that was saved in the database
// and prompted the user to report the error.
var savedLog = await RetrieveSavedLog(id);
// UpdateLogData is a helper method we provide. It updates the saved log
// with the information gathered from the user.
savedLog = UpdateLogData(savedLog, data);
// Here you can update the modified log in the database.
await UpdateLogInDatabase(savedLog);
}
}
For this functionality to work, you also need to implement the corresponding controller (ref).
Specifications
A specification is used to centralize and redistribute business logic to other parts of the application. They should be created inside
the project .Domain, in a folder named Specifications. Every specification should inherit from BaseSpecification<TEntity>.
Methods Provided
| Method | Signature | Description |
|---|---|---|
| And | ISpecification<TEntity> And(ISpecification<TEntity> specification) | AND logical operator. Forces the current AND informed specifications to be satisfied. |
| AndNot | ISpecification<TEntity> AndNot(ISpecification<TEntity> specification) | AND NOT logical operator. Forces the current specification to be satisfied AND the informed one NOT to be satisfied. |
| Or | ISpecification<TEntity> Or(ISpecification<TEntity> specification) | OR logical operator. Forces the current OR informed specifications to be satisfied. |
| OrNot | ISpecification<TEntity> OrNot(ISpecification<TEntity> specification) | OR NOT logical operator. Forces the current specification to be satisfied OR the informed one NOT to be satisfied. |
| Not | ISpecification<TEntity> Not() | Negates the current specification. |
Customization
Here you can see all the fields allowing customization of the process and how to properly use them.
| Config | Description |
|---|---|
| PageSize | Page size. |
| OrderBy | Function to sort the items. |
| Include | Function to include navigational properties. |
| DisableTracking | Whether the changes tracker should be disabled. |
Usage
Creation:
public class DoctorsWhoMadeTransfersInPastMonthsSpecification
: BaseSpecification<HistoryItem>
{
private readonly int _pastMonths;
public DoctorsWhoMadeTransfersInPastMonthsSpecification(int pastMonths = 3)
{
_pastMonths = pastMonths;
PageSize = 10;
Include = historyItem => historyItem.Include(hi => hi.Doctor);
OrderBy = historyItem => historyItem.OrderBy(hi => hi.CreationDate)
}
public override Expression<Func<CaseHistoryItem, bool>> IsSatisfiedBy()
{
var startDate = DateTime.UtcNow.AddMonths(-_pastMonths);
return historyItem => historyItem.IsTransferRelated
&& historyItem.DoctorId != null
&& historyItem.CreationDate >= startDate;
}
}
Invoking:
var records = UnitOfWork.GetRepository<HistoryItem>()
.FilterBySpec(new DoctorsWhoMadeTransfersInPastMonthsSpecification(6));
var doctorsWhoRequestedTransferInPast6Months = UnitOfWork.GetRepository<HistoryItem>()
.FilterBySpecAsQueryable(new DoctorsWhoMadeTransfersInPastMonthsSpecification(6))
.Select(ch => ch.Doctor);
var records = UnitOfWork.GetRepository<HistoryItem>()
.FilterBySpec(spec1.And(spec2).OrNot(spec3).Or(spec4));
Additional Context
The UpDevs SDK allows for multiple database contexts where necessary. You can override the default, should you need it, or add extra ones.
To override the default one:
public class MyDomainService : BaseEntityDomainService<MyEntity, MyInput, MyResponse>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork<MySecondaryDatabaseContext> unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource) { }
// ... Other methods
}
To maintain the default behavior, but still use different context(s):
public class MyDomainService : BaseEntityDomainService<MyEntity, MyInput, MyResponse>,
IMyDomainService
{
private readonly IUnitOfWork<MySecondaryDatabaseContext> _unitOfWorkSecondaryContext;
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
IUnitOfWork<MySecondaryDatabaseContext> secondaryUnitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource)
{
_unitOfWorkSecondaryContext = secondaryUnitOfWork;
}
// ... Other methods
}
You can add as many contexts as you may need.