Skip to main content

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:

MethodSignatureDescription
ToEntityTEntity ToEntity<TEntity>(object source)Maps the source object to the entity type requested.
ToResponseTResponse ToResponse<TResponse>(object source)Maps the source object (entity) to the response type requested.
MapToTResponse MapTo<TResponse>(object source)Maps the source object to the type requested.
GetDomainServiceTDomainService GetDomainService<TDomainService>()Gets a domain service.
tip

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

YourProject.Contracts/Domain/IMyDomainService.cs
public interface IMyDomainService : IDomainService
{
// ... Methods' signatures
}

Implementation

YourProject.Domain/Services/MyDomainService.cs
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

info

They all have async options - ending with Async.

MethodSignatureDescription
ListPagedListModel<TResponse> List(SearchRequest? searchModel)Returns the paged list of records using the specified filters.
GetByIdsTResponse[] GetByIds<TId>(TId[] ids)Retrieves the records matching the provided identifiers.
protected UpdateWithFilterSearchRequest 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 GetSoftDeleteFilterColumnFilter GetSoftDeleteFilter()Gets the filter to exclude soft deleted records from the search.
protected GetByIdColumnFilterColumnFilter GetByIdColumnFilter(object value, object? operand = null)Gets the filter to search records by their ID.
protected GetOperandInForDataTypeobject? GetOperandInForDataType(DataType dataType)Gets the proper IN operand according to the specified data type.
protected GetSoftDeleteRequestSearchRequest GetSoftDeleteRequest()Gets a search request with a filter to exclude soft deleted records from the search.

Interface

YourProject.Contracts/Domain/IMyDomainService.cs
public interface IMyDomainService : IListDomainService<MyResponse>
{
// ... Other methods' signatures
}

Implementation

YourProject.Domain/Services/MyDomainService.cs
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.

ConfigDescription
IncludeRelationships to be included while returning data from the list methods. If not set (default behavior), returns only the main entity.
ListSearchExpressionAllows 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.
ListExtraQueryAllows to customize the text search. The search request can be modified in this function where you also have access to the request's session.
ListExcludedFiltersNames of the filters in the request that should be excluded from the search.
ListUseFindOnGetByIdsWhether a simple .Find() should be used for the GetByIds method (retrieve record). If true, no relationships will be loaded.
ListGetByIdIncludesRelationships to be included while returning data from the GetById methods. If not set (default behavior), returns only the main entity.
ShouldSoftDeleteWhether the soft delete (logical removal) should be used instead of the physical one.
SoftDeletePropertyNameName of the property used to identify the existence status of the record. Only requires setting if different than "IsDeleted".
IgnoreSoftDeletedInListGetByIdsWhether the records that were soft deleted should be ignored from the GetByIds call.
IgnoreSoftDeletedInListWhether the records that were soft deleted should be ignored from the List call.
IdPropertyNameName of the property used as identifier. Defaults to "Id".
IdDataTypeData type of the property used as identifier. Defaults to DataType.Guid

Usage:

YourProject.Domain/Services/MyDomainService.cs
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
}

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

info

They all have async options - ending with Async. Also includes the methods in BaseListDomainService.

MethodSignatureDescription
SearchPagedListModel<TResponse> Search(SearchRequest? searchModel)Returns the paged list of records using the specified filters.

Interface

YourProject.Contracts/Domain/IMyDomainService.cs
public interface IMyDomainService : ISearchDomainService<MyResponse>
{
// ... Other methods' signatures
}

Implementation

YourProject.Domain/Services/MyDomainService.cs
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.

ConfigDescription
SearchExpressionAllows 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.
ExtraQueryAllows to customize the search. The search request can be modified in this function where you also have access to the request's session.
ExcludedFiltersNames of the filters in the request that should be excluded from the search.
IgnoreSoftDeletedInSearchWhether the records that were soft deleted should be ignored from the Search call.

Usage:

YourProject.Domain/Services/MyDomainService.cs
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

info

They all have async options - ending with Async. Also includes the methods in BaseSearchDomainService.

MethodSignatureDescription
GetTResponse? Get(params object[] keyValues)Retrieves an entity using its ID(s).
GetTResponse? Get<TId>(TId id)Retrieves an entity using its ID.
CreateTResponse Create(TInput data, bool shouldSave = true)Inserts a new record in the database using the model provided by the input.
CreateTResponse Create(TInput data, IFormFileCollection files, bool shouldSave = true)Inserts a new record in the database using the model provided by the input and files.
UpdateTResponse Update(TInput data, bool shouldSave = true)Updates the informed record in the database using the model provided by the input.
UpdateTResponse Update(TInput data, IFormFileCollection files, bool shouldSave = true)Updates the informed record in the database using the model provided by the input and files.
Deletebool Delete(params object[] keyValues)Removes a record from the database matching the composite primary key provided.
DeleteManybool DeleteMany<TId>(IEnumerable<TId> ids)Removes records from the database matching the IDs provided.

Interface

note

If you use the Model pattern instead of Input/Response for this entity, you only need to provide the model type once.

YourProject.Contracts/Domain/IMyDomainService.cs
public interface IMyDomainService : IEntityDomainService<MyModel>
{
// ... Other methods' signatures
}
YourProject.Contracts/Domain/IMyDomainService.cs
public interface IMyDomainService : IEntityDomainService<MyInput, MyResponse>
{
// ... Other methods' signatures
}

Implementation

note

If you use the Model pattern instead of Input/Response for this entity, you only need to provide the model type once.

YourProject.Domain/Services/MyDomainService.cs
public class MyDomainService : BaseEntityDomainService<MyEntity, MyModel>,
IMyDomainService
{
public MyDomainService(
IServiceProvider serviceProvider,
IUnitOfWork unitOfWork,
DomainExceptionMessagesResource domainExceptionMessagesResource
) : base(serviceProvider, unitOfWork, domainExceptionMessagesResource) { }

// ... Other methods
}
YourProject.Domain/Services/MyDomainService.cs
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.

ConfigDescription
SetupEntityRelationsConfigures the collections of the entity. Required when adding relationship items in collections.
GetByIdQueryFunction to customize the query used to retrieve a record by its ID.
HandleFilesHandles 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.
IgnoreSoftDeletedInGetByIdWhether the records that were soft deleted should be ignored from the GetById call.
ShouldClearEntityCollectionsWhether the entity collections should be cleared before invoking the transformations (SetupEntityRelations).
UseFindOnGetWhether a simple .Find() should be used for the Get method (retrieve record). If true, no relationships will be loaded.
GetByIdIncludesCustomized includes to be loaded when calling GetById.

Usage:

YourProject.Domain/Services/MyDomainService.cs
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:

NameTypeDescription
inputData type used for the inputModel with the input data provided by the user.
isEditbooleanWhether the record is being created or updated.
filesIFormFileCollectionIf 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.

YourProject.Domain/Services/MyDomainService.cs
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

YourProject.Domain/Services/MyDomainService.cs
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);
}
}
note

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

MethodSignatureDescription
AndISpecification<TEntity> And(ISpecification<TEntity> specification)AND logical operator. Forces the current AND informed specifications to be satisfied.
AndNotISpecification<TEntity> AndNot(ISpecification<TEntity> specification)AND NOT logical operator. Forces the current specification to be satisfied AND the informed one NOT to be satisfied.
OrISpecification<TEntity> Or(ISpecification<TEntity> specification)OR logical operator. Forces the current OR informed specifications to be satisfied.
OrNotISpecification<TEntity> OrNot(ISpecification<TEntity> specification)OR NOT logical operator. Forces the current specification to be satisfied OR the informed one NOT to be satisfied.
NotISpecification<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.

ConfigDescription
PageSizePage size.
OrderByFunction to sort the items.
IncludeFunction to include navigational properties.
DisableTrackingWhether the changes tracker should be disabled.

Usage

Creation:

YourProject.Domain/Specifications/DoctorsWhoMadeTransfersInPastMonthsSpecification.cs
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:

YourProject.Domain/Services/MyDomainService.cs
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):

YourProject.Domain/Services/MyDomainService.cs
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.