Connect a .NET Desktop Client to a Secure Backend Web API Service (EF Core with OData)
- 5 minutes to read
This help topic explains the fundamentals of designing a distributed application with a separate backend and frontend, focusing on a WinForms UI client and ASP.NET Core Web API based on OData v4, Entity Framework Core (EF Core), and the DevExpress Web API Service as the backend.
In distributed systems, separating the backend and frontend is extremely important for several reasons:
Security-First Design
Separation of the backend and frontend ensures that sensitive database information (such as connection strings) is protected. The WinForms UI client communicates with the server only through secure API endpoints, meaning that it cannot directly access or modify the database. Built-in security with authentication and role-based access control (RBAC) ensures that only authorized users can view or modify data based on their roles.
Cross-Platform Support and Reusability
A centralized backend API allows you to reuse business logic across multiple platforms, such as desktop (WinForms, WPF), web (Blazor, Angular, React, Vue), or mobile (.NET MAUI). This reduces the need for code duplication and makes applications easier to maintain and extend.
API Flexibility with OData
OData offers a robust and flexible solution for building RESTful APIs, which reduces development time and application complexity while delivering advanced data functionality, such as advanced filtering, sorting, and pagination out of the box.
Key Components
ASP.NET Core Web API
The ASP.NET Core Web API acts as a backend service. It handles all data operations, business logic, and security. The backend service exposes a set of endpoints for interacting with UI clients.
Entity Framework Core (EF Core) and OData v4
EF Core acts as an object-relational mapper (ORM). EF Core simplifies data access and seamlessly manages CRUD operations. OData (Open Data Protocol) is easily integrated with EF Core, and is used to query and manipulate data through RESTful services. OData allows UI clients to filter, paginate, and perform other data operations directly through the API.
DevExpress Web API Service
The DevExpress Web API Service extends the capabilities of ASP.NET Core Web API. It introduces data shaping (filtering, grouping, pagination) and integrated OData support out of the box, streamlining software development and reducing the time it takes to build robust APIs.
WinForms UI Client
The WinForms UI serves as the desktop application frontend. By separating the frontend from the backend, you ensure that the user interface only communicates with the server through APIs without direct access to the database.
Demo App for .NET 8
Our sample Windows Forms application demonstrates the following:
- How to build a data model for application business entities and security policies with EF Core.
- How to securely load data from OData endpoints to a WinForms UI client using the DevExpress Data Grid.
- How to activate authentication and authorization for the WinForms application using Web API endpoints (powered by the DevExpress Backend Web API Service).
- How to create a login form.
- How to customize UI/UX for a given user based on associated access permissions.
Play the following animation to see the result:
Implementation Details
Frontend (WinForms)
Implements a Login form.
public string Token { get; private set; } async void Login_Click(object sender, EventArgs e) { string authToken = await ClientSecurity.Authenticate(userNameEdit.Text, passwordEdit.Text); if(authToken == null) { XtraMessageBox.Show("Authentication failed."); DialogResult = DialogResult.Cancel; } else { Token = authToken; DialogResult = DialogResult.OK; } }
- Implements a responsive desktop UI.
Uses the ODataInstantFeedbackSource to bind the WinForms Data Grid to the OData service in Instant Feedback Mode.
void oDataInstantFeedbackSource1_GetSource(object sender, DevExpress.Data.ODataLinq.GetSourceEventArgs e) { // Instantiate a new DataContext this.dataContext = new Default.Container(new System.Uri(ClientSecurity.ODataUrl)); dataContext.BuildingRequest += DataContext_BuildingRequest; dataContext.ReceivingResponse += DataContext_ReceivingResponse; // Assign a query to the ODataInstantFeedbackSource e.Query = dataContext.OrderItem; } void DataContext_ReceivingResponse(object sender, Microsoft.OData.Client.ReceivingResponseEventArgs e) { Debug.WriteLine("HTTP:" + e.ResponseMessage.StatusCode); if(e.ResponseMessage.StatusCode != 200) { // Process HTTP errors } } void DataContext_BuildingRequest(object sender, Microsoft.OData.Client.BuildingRequestEventArgs e) { ClientSecurity.EnsureAuthorization(e); Debug.WriteLine("URL:" + e.RequestUri); }
- Displays data fetched from the backend securely without a direct database connection.
Backend (ASP.NET Core Web API + OData)
- Handles data access through EF Core and exposes data through RESTful services using OData v4.
- Supports flexible querying, filtering, and pagination through OData.
Implements authentication, authorization, and business logic to ensure that only authorized users can access and manipulate data.
// user.Roles.Add(defaultRole); // user.Roles.Add(adminRole); private PermissionPolicyRole CreateAdminRole() { PermissionPolicyRole adminRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(r => r.Name == "Administrators"); if(adminRole == null) { adminRole = ObjectSpace.CreateObject<PermissionPolicyRole>(); adminRole.Name = "Administrators"; adminRole.IsAdministrative = true; } return adminRole; } private PermissionPolicyRole CreateDefaultRole() { PermissionPolicyRole defaultRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(role => role.Name == "Default"); if(defaultRole == null) { defaultRole = ObjectSpace.CreateObject<PermissionPolicyRole>(); defaultRole.Name = "Default"; defaultRole.AddObjectPermissionFromLambda<ApplicationUser>(SecurityOperations.Read, cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow); defaultRole.AddNavigationPermission(@"Application/NavigationItems/Items/Default/Items/MyDetails", SecurityPermissionState.Allow); defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "ChangePasswordOnFirstLogon", cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow); defaultRole.AddMemberPermissionFromLambda<ApplicationUser>(SecurityOperations.Write, "StoredPassword", cm => cm.ID == (Guid)CurrentUserIdOperator.CurrentUserId(), SecurityPermissionState.Allow); defaultRole.AddTypePermissionsRecursively<PermissionPolicyRole>(SecurityOperations.Read, SecurityPermissionState.Allow); defaultRole.AddObjectPermissionFromLambda<OrderItem>(SecurityOperations.ReadOnlyAccess, oi => true, SecurityPermissionState.Allow); } return defaultRole; }