Introduction
WCF is really powerful, but usually developers spend so many hours developing services in order to create REST APIs exposing their database. In this tip, we'll see how to build REST APIs based on WCF, without having to code the services neither the business objects, using Supido.
Defining API with XML File
This is the example XML using for this tip:
="1.0"="utf-8"="Supido.Demo.Service.Security.SessionManager"="Supido.Demo.Service.Security.SecurityManager"="true"="api"="client"="ClientDto"="ClientId"="project"="ProjectDto"="ProjectId"="service"="ServiceDto"="ServiceId"="task"="TaskDto"="TaskId"="department"="DepartmentDto"="DepartmentId"
As we can see, we have two sections:
- One to define the security behaviour of our application
- Other to define our service API:
- if the service allow CORS
- the API prefix path
- The API tree. For each node, we define the route path, the name of the DTO object and the name of the
keyparameter
The previous XML will automatically traduce to a service API, without coding the services, only the DTOs are needed:
- GET /api/client
- GET /api/client/{clientId}
- POST /api/client/query (it's a get but receiving a query object)
- POST /api/client/{clientId}/query (it's a get but receiving a query object)
- POST /api/client
- PUT /api/client
- DELETE /api/client/{clientId}
- GET /api/client/{clientId}/project
- GET /api/client/{clientId}/project/{projectId}
- POST /api/client/{clientId}/project/query (it's a get but receiving a query object)
- POST /api/client/{clientId}/project/{projectId}/query (it's a get but receiving a query object)
- POST /api/client/{clientId}/project
- PUT /api/client/{clientId}/project
- DELETE /api/client/{clientId}/project/{clientId}
- GET /api/client/{clientId}/project/{projectId}/service
- GET /api/client/{clientId}/project/{projectId}/service/{serviceId}
- POST /api/client/{clientId}/project/{projectId}/service/query (it's a get but receiving a query object)
- POST /api/client/{clientId}/project/{projectId}/service/{serviceId}/query (it's a get but receiving a query object)
- POST /api/client/{clientId}/project/{projectId}/service
- PUT /api/client/{clientId}/project/{projectId}/service
- DELETE /api/client/{clientId}/project/{projectId}/service/{serviceId}
- GET /api/client/{clientId}/project/{projectId}/service/{serviceId}/task
- GET /api/client/{clientId}/project/{projectId}/service/{serviceId}/task/{taskId}
- POST /api/client/{clientId}/project/{projectId}/service/{serviceId}/task (it's a get but receiving a query object)
- POST /api/client/{clientId}/project/{projectId}/service/{serviceId}/task/{taskId} (it's a get but receiving a query object)
- POST /api/client/{clientId}/project/{projectId}/service/{serviceId}/task
- PUT /api/client/{clientId}/project/{projectId}/service/{serviceId}/task
- DELETE /api/client/{clientId}/project/{projectId}/service/{serviceId}/task/{taskId}
- GET /api/department
- GET /api/department/{departmentId}
- POST /api/department/query (it's a get but receiving a query object)
- POST /api/department/{departmentId}/query (it's a get but receiving a query object)
- POST /api/department
- PUT /api/department
- DELETE /api/department
Security
All the services require a sessionToken
in order to work. Supido provides two base classes to build your security system:
BaseSessionManager
: controls all the sessions of the users, its able to create new sessions and to recover session data from the sessionToken
. Example:
public class SessionManager : BaseSessionManager
{
public SessionManager()
: base(typeof(Session), typeof(SessionDto))
{
}
protected override object GetSessionByToken(OpenAccessContext context, string sessionToken)
{
return context.GetAll<Session>().Where(p => p.SessionToken == sessionToken).FirstOrDefault();
}
protected override IList GetAllSessionsOfUser(OpenAccessContext context, long userId)
{
return context.GetAll<Session>().Where(p => p.UserId == userId).ToList();
}
protected override object NewSession(long userId, string sessionToken)
{
Session session = new Session();
session.SessionToken = sessionToken;
session.UserId = Convert.ToInt32(userId);
session.CreationDttm = DateTime.UtcNow;
session.UpdateDttm = DateTime.UtcNow;
return session;
}
protected override void UpdateSessionAccess(object session)
{
(session as Session).UpdateDttm = DateTime.UtcNow;
}
}
BaseSecurityManager
public class SecurityManager : BaseSecurityManager
{
public SecurityManager(string fileName)
: base(typeof(EntitiesModel), typeof(UserDto), typeof(User), fileName)
{
}
protected override object GetUserByLoginPass
(OpenAccessContext context, string login, string password)
{
return context.GetAll<User>().Where
(p => p.Email == login && p.Password == password).FirstOrDefault();
}
protected override object GetUserById(OpenAccessContext context, long userId)
{
return context.GetAll<User>().Where(p => p.UserId == userId).FirstOrDefault();
}
#endregion
}
Queryable from the frontend
Supido creates two special operations for the POST
verb: one for the GetAll
and one for the GetOne
, both with the url finished with /query
. This is made in order to recibe a Query Information serialized object from the frontend, that Supido will convert to the lambda expression for the query to the database.
This Query Information is based on facets
:
{
"Facets": [
{
"Name": "Position",
"Values": [
{
"Operation": "GreaterThan",
"Value": "10"
}
]
},
{
"Name": "Priority",
"Values": [
{
"Operation": "Equal",
"Value": "1"
},
{
"Operation": "Equal",
"Value": "3"
}
]
}
],
"Orders": [
{
"Name": "Detail",
"IsAscending": true
}
],
"SkipRecords": 100,
"TakeRecords": 50
}
The example query will traduce the facets to: WHERE (Position > 10) AND (Priority = 1 OR Priority = 3)
. Also will add the order and bring only 50 records skipping 100 records (pagination).
Change the Behavior
Sometimes, we want to change the behavior of our business objects. In this case, we have two options:
- Use filters, based on the
BaseContextBOFilter abstract
class. We can specify that a class is a Filter
with the [Filter]
attribute. This attribute will resolve automatically the DTO from the name, or we can pass as parameter the DTO type. The filters allow us to interact with the business objects in two ways:
- Adding security to the queries, with the method
ApplySecurity
. We receive the information about the user that is executing the operation, so we can modify the query in order to contextualize it with the security information of the user. - Modifying the way of using the
QueryInfo
with the method ApplyFilter
. By default, the method ApplyFilter
is able to resolve the where, order by and pagination information. We can change this behaviour to add our own way to query the information.
- Inherit from
ContextBO<TDto>
and build our own BO, changing the behaviour of the operations that we want overriding them, or subscribing to the events that happen before and after each operation and that allow us to intercept and modify the information. When we write our own BO, we must use the Attribute [BO], that will resolve the DTO type by the name of the BO, or we can specify the DTO type as parameter to the attribute.
How It Works?
When the service start, the application is scanned searching for the [DTO], [Filter] and [BO] attributes, and build the information inside a Metamodel. An ISessionManager
and an ISecurityManager
are created. After that, the API defined in the XML are created: for each one if a BO or Filter is defined by the user, then uses the user ones, otherwise it creates generic Filter and BO based on the default behaviour. Then, a RestService
class is created for the high level nodes of the API, defining the base operations for all the URI templates. When an operation is called, internally it resolves the path and the query parameters from the Message
, and search internally the correct BO to use, passing the URL path parameters as a build query.
So, when we call:
GET /api/client/{clientId}/project/{projectId}/service/{serviceId}/task
GET /api/client/7/project/2/service/3/task
Internally, a query is created with the parameters:
Service.ServiceId = 3
Service.Project.ProjectId = 2
Service.Project.Client.ClientId = 7
And knows that the call is a GetAll
because there is no last parameter, and that the DTO is TaskDto, as defined in the XML, so the correct BO and Filter are resolved for the TaskDto.
Using the Code
There is a demo application showing how to use the code. The code works using Telerik Data Access for the data layer. You can open the project and tell telerik to generate the database (right-click over the model, update database from model), or use the database script provided.