Connect a .NET Desktop Client to a Backend Using a Middle Tier Server (EF Core without OData)
- 5 minutes to read
A middle-tier architecture combines all data access and related logic in one place, adding an essential layer of security as it can manage authentication, authorization, and encryption. With this additional layer of protection, desktop UI clients cannot access database connection information or modify database tables directly. This separation simplifies client application development and reduces code duplication, as the same logic can be reused by multiple UI clients, making the ‘system’ easier to maintain and scale.
In a Middle Tier Server architecture, DbContext and other EF Core CRUD APIs are still used in the UI client app code, which interacts with the server remotely. Only the server has direct access to the database. Before passing data to the client’s DbContext, the server enforces security measures such as authentication, authorization, and data validation.
Key Components
Entity Framework Core (EF Core)
EF Core acts as an object-relational mapper (ORM). EF Core simplifies data access and seamlessly manages CRUD operations.
Middle Tier Server (Data Access and Business Logic Layer)
This layer acts as an intermediary between the WinForms UI client and the database. The DevExpress ASP.NET Core Middle Tier Security Server allows EF Core developers to maintain their standard DbContext while establishing remote and secure database connections from .NET clients. It “replaces” direct database connections with middleware, ensuring that only authorized users can access specific/sensitive data or perform granted actions.
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 use the DevExpress ASP.NET Core Middle Tier Security Server to connect a Windows Forms .NET 8 client application to a backend.
- How to define access permissions and activate authentication/authorization for a .NET 8 WinForms application.
- How to create a login form.
- How to customize the UI/UX for a given user based on associated access permissions.
- How to create an edit form to modify and save data (CRUD).
Implementation Details
Initialization and Authentication
The RemoteContextUtils
class implements utility APIs related to remote DbContext and secured communication within client applications. The CreateSecuredClient
method securely connects to an EF Core Middle Tier Security application:
public static WebApiSecuredDataServerClient CreateSecuredClient(string url, string login, string password) {
var httpClient = new HttpClient();
httpClient.BaseAddress = new Uri(url);
httpClient.DefaultRequestHeaders.Add("Accept", "application/json");
var securedClient = new WebSocketSecuredDataServerClient(httpClient, XafTypesInfo.Instance);
securedClient.CustomAuthenticate += (sender, arguments) => {
arguments.Handled = true;
HttpResponseMessage msg = arguments.HttpClient.PostAsJsonAsync("api/Authentication/Authenticate", (AuthenticationStandardLogonParameters)arguments.LogonParameters).GetAwaiter().GetResult();
string token = (string)msg.Content.ReadFromJsonAsync(typeof(string)).GetAwaiter().GetResult();
if(msg.StatusCode == HttpStatusCode.Unauthorized) {
throw new UserFriendlyException(token);
}
msg.EnsureSuccessStatusCode();
arguments.HttpClient.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("bearer", token);
};
securedClient.Authenticate(new AuthenticationStandardLogonParameters(login, password));
((IMiddleTierServerSecurity)securedClient).Logon();
return securedClient;
}
The RemoteContextUtils.GetDBContext()
method returns a secured DbContext for CRUD operations:
public static DXApplication1EFCoreDbContext GetDBContext() {
return new DXApplication1EFCoreDbContext(Options);
}
public static DbContextOptions<DXApplication1EFCoreDbContext> CreateDbContextOptions(WebApiSecuredDataServerClient securedClient) {
var builder = new DbContextOptionsBuilder<DXApplication1EFCoreDbContext>();
return builder
.UseLazyLoadingProxies()
.UseChangeTrackingProxies()
.UseMiddleTier(securedClient)
.Options as DbContextOptions<DXApplication1EFCoreDbContext>;
}
Access Permissions
The following code example creates Admin and User roles and defines these permissions:
- Admin: Read/Write (full access)
- User: Read Only (data editing is not allowed)
public class Updater : ModuleUpdater {
private const string AdministratorUserName = "Admin";
private const string AdministratorRoleName = "Administrators";
private const string DefaultUserName = "User";
private const string DefaultUserRoleName = "Users";
//...
private PermissionPolicyRole GetUserRole() {
PermissionPolicyRole userRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(u => u.Name == DefaultUserRoleName);
if(userRole == null) {
userRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
userRole.Name = DefaultUserRoleName;
userRole.AddTypePermission<Employee>(SecurityOperations.Read, SecurityPermissionState.Allow);
userRole.AddTypePermission<Employee>(SecurityOperations.Write, SecurityPermissionState.Deny);
}
return userRole;
}
private PermissionPolicyRole GetAdminRole() {
PermissionPolicyRole adminRole = ObjectSpace.FirstOrDefault<PermissionPolicyRole>(u => u.Name == AdministratorRoleName);
if(adminRole == null) {
adminRole = ObjectSpace.CreateObject<PermissionPolicyRole>();
adminRole.Name = AdministratorRoleName;
adminRole.IsAdministrative = true;
}
return adminRole;
}
//...
}
Login Form: Validate User Credentials, User Authorization
The LogIn form attempts to log the user into the security system and returns DialogResult.OK
if a login was successful:
using(AuthForm authForm = new AuthForm()) {
if(authForm.ShowDialog() == DialogResult.OK) {
MiddleTierStartupHelper.WaitMiddleTierServerReady(MiddleTierStartupHelper.EFCoreWebApiMiddleTierInstanceKey, TimeSpan.MaxValue);
// User authorization.
var securedClient = RemoteContextUtils.CreateSecuredClient(System.Configuration.ConfigurationManager.AppSettings["endpointUrl"], authForm.Login, authForm.Password);
RemoteContextUtils.SecuredDataServerClient = securedClient;
DbContextOptions<DXApplication1EFCoreDbContext> options = RemoteContextUtils.CreateDbContextOptions(securedClient);
RemoteContextUtils.Options = options;
Application.Run(new MainForm());
}
else
break;
}
Bind the DevExpress Data Grid to Remote Data
If user authentication was successful, the SetUpBinding
method creates a DbContext and binds the grid to data:
public partial class MainForm : RibbonForm {
EntityServerModeSource serverModeSource = new EntityServerModeSource();
DXApplication1EFCoreDbContext dbContext = null;
public MainForm() {
InitializeComponent();
SetUpBinding();
//...
}
void SetUpBinding() {
dbContext?.Dispose();
dbContext = RemoteContextUtils.GetDBContext();
serverModeSource = new EntityServerModeSource() { ElementType = typeof(Employee), KeyExpression = "ID" };
serverModeSource.QueryableSource = dbContext.Employees;
gridControl.DataSource = serverModeSource;
}
}
Configure UI Based on Access Permissions
Highlighted lines enable/disable Ribbon commands (New, Edit, Delete) based on access permissions of the current (logged in) user:
public MainForm() {
InitializeComponent();
SetUpBinding();
bbiNew.Enabled = RemoteContextUtils.IsGranted(typeof(Employee), SecurityOperations.Create);
bbiDelete.Enabled = RemoteContextUtils.IsGranted(typeof(Employee), SecurityOperations.Delete);
bbiEdit.Enabled = RemoteContextUtils.IsGranted(typeof(Employee), SecurityOperations.Write);
}