Skip to main content
A newer version of this page is available. .
All docs
V21.2
.NET Standard 2.0+

Add the Middle Tier Server (ASP.NET Core Web API) to an existing WinForms Application (.NET 5+) (CTP)

  • 7 minutes to read

Important

We do not recommend that you use the Backend Web API Service in production code. We made this service available to gather feedback from anyone who considers it for future use.

This topic contains step-by-step instructions on how to add a Web API project to a WinForms application and use it as the Middle Tier Server.

  1. Use the Solution Wizard to add a Web API project to your application. Right-click the solution in the Solution Explorer and choose Add DevExpress Item | New Project….

    Solution

  2. Select the .NET Core platform and run the XAF Solution Wizard.

    Tutorial launch Wizard

  3. Choose the ASP.NET Core Web API Service option and specify the project name.

    Tutorial launch Wizard

  4. Select the eXpress Persistent Objects ORM for your application. The current Web API version does not support Entity Framework Core.

    Select ORM

  5. Choose Standard authentication on the Choose Security page. The wizard generates JWT authentication scaffolding code.

    Enable the JWT authentication

    The wizard adds the MySolution.WebApi project to the solution.

  6. Make sure that the DevExpress.ExpressApp.Security.Xpo NuGet package is installed to the MySolution.Win and MySolution.WebApi projects.

    See the following topic for details: Install DevExpress Controls Using NuGet Packages.

  7. Create the Controllers folder in the MySolution.WebApi project and add the WebApiMiddleTierController.cs file to this folder.

    File: MySolution.WebApi\Controllers\WebApiMiddleTierController.cs

    using System;
    using System.Collections.Generic;
    using DevExpress.ExpressApp.Blazor.AmbientContext;
    using DevExpress.ExpressApp.Security;
    using DevExpress.ExpressApp.Security.ClientServer;
    using DevExpress.ExpressApp.WebApi.Services;
    using DevExpress.Xpo;
    using DevExpress.Xpo.Helpers;
    using Microsoft.AspNetCore.Authorization;
    using Microsoft.AspNetCore.Mvc;
    
    namespace MySolution.WebApi.Controllers {
        [ApiController]
        [Route("[controller]/[action]")]
        [Produces("application/xml")]
        public class MiddleTierController : ControllerBase {
            readonly ISecurityAuthenticationService authenticationService;
            readonly WebApiSecuredDataServer dataServer;
            readonly IValueManagerStorageContext valueManagerStorageContext;
    
            public MiddleTierController(ISecurityAuthenticationService authenticationService, WebApiSecuredDataServer dataServer, IValueManagerStorageContext valueManagerStorageContext) {
                this.authenticationService = authenticationService;
                this.dataServer = dataServer;
                this.valueManagerStorageContext = valueManagerStorageContext;
            }
            [Authorize]
            [HttpPost]
            public OperationResult<SerializableSecurityObjectResult> LoadObjects([FromBody] SerializableObjectLayerDataContainer<ObjectStubsQuery[]> data) {
                return RunWithStorage(() => dataServer.LoadObjects(data.Dictionary, data.Data));
            }
            [Authorize]
            [HttpPost]
            public OperationResult<SerializableSecurityObjectResult> CommitObjects([FromBody] SerializableObjectLayerDataContainer<CommitObjectContainer> data) {
                return RunWithStorage(() => dataServer.CommitObjects(data.Dictionary, data.Data.ObjectsForDelete, data.Data.ObjectsForSave, data.Data.LockingOption, data.Data.SecurityObjectsData));
            }
            [Authorize]
            [HttpPost]
            public OperationResult<SerializableSecurityObjectResult> GetObjectsByKey([FromBody] SerializableObjectLayerDataContainer<GetObjectStubsByKeyQuery[]> data) {
                return RunWithStorage(() => dataServer.GetObjectsByKey(data.Dictionary, data.Data));
            }
            [Authorize]
            [HttpPost]
            public OperationResult<object[][]> SelectData([FromBody] SerializableObjectLayerDataContainer<SelectDataContainer> data) {
                return RunWithStorage(() => dataServer.SelectData(data.Dictionary, data.Data.Query, data.Data.Properties, data.Data.GroupProperties, data.Data.GroupCriteria));
            }
            [Authorize]
            [HttpGet]
            public bool CanLoadCollectionObjects() {
                return dataServer.CanLoadCollectionObjects();
            }
            [Authorize]
            [HttpPost]
            public OperationResult<SerializableSecurityObjectResult> LoadCollectionObjects([FromQuery] string refPropertyName, [FromBody] SerializableObjectLayerDataContainer<XPObjectStub> data) {
                return RunWithStorage(() => dataServer.LoadCollectionObjects(data.Dictionary, refPropertyName, data.Data));
            }
            [Authorize]
            [HttpPost]
            public OperationResult<PurgeResult> Purge() {
                return RunWithStorage(() => dataServer.Purge());
            }
            [Authorize]
            [HttpPost]
            public OperationResult<SerializableObjectLayerResult<object[]>> LoadDelayedProperties([FromQuery] string[] props, [FromBody] SerializableObjectLayerDataContainer<XPObjectStub> data) {
                return RunWithStorage(() => dataServer.LoadDelayedProperties(data.Dictionary, data.Data, props));
            }
            [Authorize]
            [HttpPost]
            public OperationResult<SerializableObjectLayerResult<object[]>> LoadDelayedProperty([FromQuery] string property, [FromBody] SerializableObjectLayerDataContainer<XPObjectStubCollection> data) {
                return RunWithStorage(() => dataServer.LoadDelayedProperties(data.Dictionary, data.Data, property));
            }
            [Authorize]
            [HttpPost]
            public bool IsParentObjectToSave([FromBody] SerializableObjectLayerDataContainer<XPObjectStub> data) {
                return dataServer.IsParentObjectToSave(data.Dictionary, data.Data);
            }
            [Authorize]
            [HttpPost]
            public bool IsParentObjectToDelete([FromBody] SerializableObjectLayerDataContainer<XPObjectStub> data) {
                return dataServer.IsParentObjectToDelete(data.Dictionary, data.Data);
            }
            [Authorize]
            [HttpGet]
            public SerializableObjectLayerResult<XPObjectStubCollection> GetParentObjectsToSave() {
                return dataServer.GetParentObjectsToSave();
            }
            [Authorize]
            [HttpGet]
            public SerializableObjectLayerResult<XPObjectStubCollection> GetParentObjectsToDelete() {
                return dataServer.GetParentObjectsToDelete();
            }
            [Authorize]
            [HttpGet]
            public string[] GetParentTouchedClassInfos() {
                return dataServer.GetParentTouchedClassInfos();
            }
            [Authorize]
            [HttpPost]
            public OperationResult CreateObjectType([FromQuery] string assemblyName, [FromQuery] string typeName) {
                return RunWithStorage(() => dataServer.CreateObjectType(assemblyName, typeName));
            }
            [Authorize]
            [HttpPost]
            public OperationResult<object> Do([FromQuery] string command, [FromBody] object args) {
                return RunWithStorage(() => dataServer.Do(command, args));
            }
            [Authorize]
            [HttpPost]
            public IActionResult Logon() {
                return NoContent();
            }
            [Authorize]
            [HttpPost]
            public IActionResult Logoff() {
                dataServer.Logoff();
                return NoContent();
            }
            [Authorize]
            [HttpPost]
            public bool IsGranted([FromBody] IPermissionRequest permissionRequest) {
                return dataServer.IsGranted(permissionRequest);
            }
            [Authorize]
            [HttpPost]
            public IList<bool> AreGranted([FromBody] IList<IPermissionRequest> permissionRequests) {
                return dataServer.IsGranted(permissionRequests);
            }
            [Authorize]
            [HttpGet]
            public object GetUserId() {
                return dataServer.GetUserId();
            }
            [Authorize]
            [HttpGet]
            public string GetUserName() {
                return dataServer.GetUserName();
            }
            [HttpGet]
            public SecurityStrategyServerInfo GetSecurityStrategyInfo() {
                return dataServer.GetSecurityStrategyServerInfo();
            }
            [Authorize]
            [HttpGet]
            public IEnumerable<IEnumerable<IOperationPermission>> GetSecurityPermissions() {
                return dataServer.GetSecurityPermissions();
            }
            T RunWithStorage<T>(Func<T> getResult) {
                T result = default;
                valueManagerStorageContext.RunWithStorage(() => result = getResult());
                return result;
            }
        }
    }
    
  8. Create the Formatters folder in the MySolution.WebApi project and add the XmlFormatters.cs file to this folder.

    File: MySolution.WebApi\Formatters\XmlFormatters.cs

    using System;
    using System.Runtime.Serialization;
    using DevExpress.ExpressApp.Security.ClientServer;
    using DevExpress.Xpo.DB.Helpers;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.AspNetCore.Mvc.Formatters;
    
    namespace MySolution.WebApi.Formatters {
        public class XafXmlDataContractSerializerInputFormatter : XmlDataContractSerializerInputFormatter {
            public XafXmlDataContractSerializerInputFormatter(MvcOptions options) : base(options) { }
            protected override DataContractSerializer CreateSerializer(Type type) {
                return WebApiDataContractXmlDataFormatter.GetSerializer(type, WebApiSecuredDataServerClient.KnownTypes);
            }
        }
        public class XafXmlDataContractSerializerOutputFormatter : XmlDataContractSerializerOutputFormatter {
            protected override DataContractSerializer CreateSerializer(Type type) {
                return WebApiDataContractXmlDataFormatter.GetSerializer(type, WebApiSecuredDataServerClient.KnownTypes);
            }
        }
    }
    
    namespace Microsoft.Extensions.DependencyInjection {
        using Microsoft.AspNetCore.Mvc.Formatters.Xml;
        using Microsoft.Extensions.Options;
        using MySolution.WebApi.Formatters;
    
        public static class MiddleTierFormattersExtenions {
            public static IMvcBuilder AddXafMiddleTierFormatters(this IMvcBuilder builder, Action<MvcXmlOptions> configure) {
                builder.Services.Add(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, XafMiddleTierFormattersMvcOptionsSetup>());
                builder.Services.Configure(configure);
                return builder;
            }
            public static IMvcBuilder AddXafMiddleTierFormatters(this IMvcBuilder builder) {
                builder.Services.Add(ServiceDescriptor.Transient<IConfigureOptions<MvcOptions>, XafMiddleTierFormattersMvcOptionsSetup>());
                return builder;
            }
        }
        sealed class XafMiddleTierFormattersMvcOptionsSetup : IConfigureOptions<MvcOptions> {
            void IConfigureOptions<MvcOptions>.Configure(MvcOptions options) {
                options.InputFormatters.Add(new XafXmlDataContractSerializerInputFormatter(options));
                options.OutputFormatters.Add(new XafXmlDataContractSerializerOutputFormatter());
            }
        }
    }
    
  9. Create the Middleware folder in the MySolution.WebApi project and add the XafAuthorizationMiddleware.cs file to this folder.

    File: MySolution.WebApi\Middleware\XafAuthorizationMiddleware.cs

    using System;
    using System.Threading.Tasks;
    using DevExpress.ExpressApp;
    using DevExpress.ExpressApp.Security;
    using DevExpress.ExpressApp.Xpo;
    using DevExpress.Xpo;
    using MySolution.WebApi.Services;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.DependencyInjection;
    using DevExpress.ExpressApp.Blazor.AmbientContext;
    
    namespace MySolution.WebApi.Middleware {
        public class XafAuthorizationMiddleware {
            readonly RequestDelegate next;
    
            public XafAuthorizationMiddleware(RequestDelegate next) {
                this.next = next;
            }
    
            public async Task Invoke(HttpContext context, IServiceProvider serviceProvider) {
                if(NeedLogon(context.Request.Path)) {
                    var dataServerSecurity = serviceProvider.GetRequiredService<IDataServerSecurity>();
                    var dataLayerAccessor = serviceProvider.GetRequiredService<MiddleTierDataLayerAccessor>();
                    var logonParametersInitializer = serviceProvider.GetRequiredService<ILogonParametersInitializer>();
                    logonParametersInitializer.Initialize((ISecurityStrategyBase)dataServerSecurity);
                    serviceProvider.GetRequiredService<IValueManagerStorageContext>().RunWithStorage(() => {
                        Authenticate(dataServerSecurity, dataLayerAccessor.GetDataLayer(serviceProvider));
                    });
                }
                await next(context);
            }
    
            void Authenticate(IDataServerSecurity dataServerSecurity, IDataLayer dataLayer) {
                if(!dataServerSecurity.IsAuthenticated) {
                    IObjectSpace objectSpace = new XPObjectSpace(XafTypesInfo.Instance, XpoTypesInfoHelper.GetXpoTypeInfoSource(), () => new UnitOfWork(dataLayer));
                    SecurityStrategyBase security = (SecurityStrategyBase)dataServerSecurity;
                    dataServerSecurity.Logon(objectSpace);
                    SecuritySystem.SetInstance(dataServerSecurity);
                }
            }
    
            static bool NeedLogon(PathString path) {
                PathString remaining;
                if(!path.StartsWithSegments("/middletier", StringComparison.OrdinalIgnoreCase, out remaining)) return false;
                if(!remaining.HasValue) return false;
                if(remaining.StartsWithSegments("/getsecuritystrategyinfo", StringComparison.OrdinalIgnoreCase)) return false;
                return true;
            }
        }
    }
    
  10. Add the MiddleTierDataLayerAccessor.cs file to the MySolution.WebApi\Services folder.

    File: MySolution.WebApi\Services\MiddleTierDataLayerAccessor.cs

    using System;
    using DevExpress.ExpressApp.Blazor;
    using DevExpress.ExpressApp.Blazor.Services;
    using DevExpress.ExpressApp.Xpo;
    using DevExpress.Xpo;
    using DevExpress.Xpo.DB;
    using DevExpress.Xpo.Metadata;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace MySolution.WebApi.Services {
        public class MiddleTierDataLayerAccessor : IDisposable {
            IDisposable[] objectsToDispose;
            IDataLayer dataLayer;
            public IDataLayer GetDataLayer(IServiceProvider serviceProvider) {
                if(dataLayer == null) {
                    BlazorApplication app = serviceProvider.GetRequiredService<IXafApplicationProvider>().GetApplication();
                    app.CheckCompatibility();
                    var dataStoreProviderAccessor = serviceProvider.GetRequiredService<XpoDataStoreProviderAccessor>();
                    IDataStore dataStore = dataStoreProviderAccessor.DataStoreProvider.CreateWorkingStore(out objectsToDispose);
                    XPDictionary dictionary = XpoTypesInfoHelper.GetXpoTypeInfoSource().XPDictionary;
                    dataLayer = new ThreadSafeDataLayer(dictionary, dataStore);
                }
                return dataLayer;
            }
            void IDisposable.Dispose() {
                if(objectsToDispose != null) {
                    foreach(IDisposable objectToDispose in objectsToDispose) {
                        objectToDispose.Dispose();
                    }
                }
            }
        }
    }
    
  11. Add the following code to the MySolution.WebApi\Startup.cs file:

    File: MySolution.WebApi\Startup.cs

    using MySolution.WebApi.Middleware;
    using DevExpress.ExpressApp.Security.ClientServer;
    using DevExpress.Xpo;
    // ...
    public void ConfigureServices(IServiceCollection services) {
    // ...    
        services.AddXafWebApi(options => {
            // Use options.BusinessObject<YourBusinessObject>() to make the Business Object available in the Web API and generate the GET, POST, PUT, and DELETE HTTP methods for it.
            });
        services.AddSingleton<MiddleTierDataLayerAccessor>();
        services.AddScoped(sp => (IDataServerSecurity)sp.GetRequiredService<ISecurityStrategyBase>());
        services.AddScoped<IMiddleTierSerializableObjectLayer>(sp => {
            var dataLayerAccessor = sp.GetRequiredService<MiddleTierDataLayerAccessor>();
            var dataServerSecurity = sp.GetRequiredService<IDataServerSecurity>();
            ISelectDataSecurity selectDataSecurity = dataServerSecurity.CreateSelectDataSecurity();
            Session session = new UnitOfWork(dataLayerAccessor.GetDataLayer(sp));
            return new MiddleTierSerializableObjectLayer(session, () => selectDataSecurity, false);
        });
        services.AddScoped<WebApiSecuredDataServer>();
        services.AddControllers().AddOData(options => {
            options
                .AddRouteComponents("api/odata", new XafApplicationEdmModelBuilder(services).GetEdmModel())
                .EnableQueryFeatures(100);
        }).AddXafMiddleTierFormatters();
    // ...    
    }
    
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
    // ...
        app.UseXaf();
        app.UseMiddleware<XafAuthorizationMiddleware>();
        app.UseEndpoints(endpoints => {
            endpoints.MapControllers();
        });
    }
    
  12. The Security System requires the ApplicationUser and ApplicationUserLoginInfo business objects to store user information. If the MySolution.Module project does not contain these objects, add the ApplicationUser and ApplicationUserLoginInfo objects to the MySolution.Module project as described in Step 1 of the following section: Use Active Directory and OAuth2 Authentication Providers in ASP.NET Core Blazor Applications: Common Steps.

  13. Add the following code to the MySolution.Win\Program.cs file:

    File: MySolution.Win\Program.cs

    using System.Net;
    using System.Net.Http.Headers;
    using System.Net.Http;
    using System.Net.Http.Json;
    using DevExpress.ExpressApp.Security.ClientServer;
    // ...
    namespace MySolution.Win {
        static class Program {
            static void Main() {
                // ...
                Tracing.Initialize();
                MySolutionWindowsFormsApplication winApplication = new MySolutionWindowsFormsApplication();
                var httpClient = new HttpClient();
                httpClient.BaseAddress = new Uri("https://localhost:44318/");
                var webApiSecuredClient = new WebApiSecuredDataServerClient(httpClient, winApplication);
                webApiSecuredClient.CustomAuthenticate += WebApiSecuredClient_CustomAuthenticate;
                var security = new WebApiMiddleTierClientSecurity(webApiSecuredClient) { IsSupportChangePassword = true };
                winApplication.Security = security;
                winApplication.CreateCustomObjectSpaceProvider += (s, e) => {
                    e.ObjectSpaceProviders.Add(new MiddleTierServerObjectSpaceProvider(webApiSecuredClient));
                };
                //...
            }
        }
        private static void WebApiSecuredClient_CustomAuthenticate(object sender, WebApiSecuredDataServerClientCustomAuthenticateEventArgs e) {
            e.Handled = true;
            HttpResponseMessage msg = e.HttpClient.PostAsJsonAsync("api/Authentication/Authenticate", (AuthenticationStandardLogonParameters)e.LogonParameters).GetAwaiter().GetResult();
            string token = msg.Content.ReadAsStringAsync().GetAwaiter().GetResult();
            if(msg.StatusCode == HttpStatusCode.Unauthorized) {
                throw new UserFriendlyException(token);
            }
            msg.EnsureSuccessStatusCode();
            e.HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);
        }
    }    
    
  14. If the MySolution.Win\Program.cs file contains the following auto-generated code, comment it out:

    File: MySolution.Win\Program.cs

    namespace MySolution.Win {
        static class Program {
            [STAThread]
            static void Main() {
                //...
                //winApplication.GetSecurityStrategy().RegisterXPOAdapterProviders();
                //...
    //#if DEBUG
    //              if(System.Diagnostics.Debugger.IsAttached && winApplication.CheckCompatibilityType == CheckCompatibilityType.DatabaseSchema) {
    //                  winApplication.DatabaseUpdateMode = DatabaseUpdateMode.UpdateDatabaseAlways;
    //              }
    //#endif
    

    You do not need to enable the Security Adapter and set the database update mode for the Middle Tier Server.

  15. Right-click the Solution node in the Solution Explorer and choose Properties. In the Solution Property Pages dialog, expand the Common Properties node and select Startup Project. Choose the Multiple Startup Projects option and select the Start action for the Web API and MySolution.Win projects.

    Run WinForms and Web API projects

    Run the application.

See Also