Example E1150
Visible to All Users

XAF - How to prevent altering the legacy database schema when creating an XAF application

This example shows how to prevent altering the legacy database schema when creating an XAF application. Sometimes our customers want to connect their XAF applications to legacy databases, but they often have strong restrictions, which disallow making any changes in the legacy database schema, i.e. adding new tables, new columns. This is bad, because XAF creates the ModuleInfo table to use an application's version for internal purposes. XPO itself can add the XPObjectType table to correctly manage table hierarchies when one persistent object inherits another one. Usually, legacy databases contain plain tables that can be mapped to one persistent object. So, the XPObjectType table is not necessary in such scenarios.

However, one problem still remains: it is the additional ModuleInfo table added by XAF itself. The idea is to move the ModuleInfo and XPObjectType tables into a temporary database.

For this task we introduced a custom IDataStoreAsync implementation, which works as a proxy. This proxy receives all the requests from the application's Session objects to a data store, and redirects them to actual XPO data store objects based upon a table name that has been passed.

[!WARNING]
We created this example for demonstration purposes and it is not intended to address all possible usage scenarios.
If this example does not have certain functionality or you want to change its behavior, you can extend this example. Note that such an action can be complex and would require good knowledge of XAF: UI Customization Categories by Skill Level and a possible research of how our components function. Refer to the following help topic for more information: Debug DevExpress .NET Source Code with PDB Symbols.
We are unable to help with such tasks as custom programming is outside our Support Service purview: Technical Support Scope.

Implementation Details

  1. In YourSolutionName.Module project create a custom IDataStoreAsync implementation as shown in the XpoDataStoreProxy.cs file;
  2. In YourSolutionName.Module project create a custom IXpoDataStoreProvider implementation as shown in the XpoDataStoreProxyProvider.cs file;
  3. For Blazor, in YourSolutionName.Blazor.Server project locate the XAF Application Builder invokation and modify the ObjectSpaceProviders initialization as shown in the Startup.cs file;
  4. For Win/Web, in YourSolutionName.Module project locate the ModuleBase descendant and modify it as shown in the Module.cs file;
  5. Define connection strings under the <connectionStrings> element in the configuration files of your WinForms and ASP.NET executable projects as shown in the WinWebSolution.Win\App.config, WinWebSolution.Win\Web.config and AspNetCore.Blazor.Server\appsettings.json files.

Important Notes

  1. The approach shown here is intended for plain database tables (no inheritance between your persistent objects). If the classes you added violate this requirement, the exception will occur as expected, because it's impossible to perform a query between two different databases by default.
  2. One of the limitations is that an object stored in one database cannot refer to an object stored in another database via a persistent property. Besides the fact that a criteria operator based on such a reference property cannot be evaluated, referenced objects are automatically loaded by XPO without involving the IDataStoreAsync.SelectData method. So, these queries cannot be redirected. As a solution, you can implement a non-persistent reference property and use the SessionThatPointsToAnotherDatabase.GetObjectByKey method to load a referenced object manually.
    3. As an alternative to the demonstrated proxy solution you can consider solutions based on database server features. Create a view mapped to a table in another database as a Synonym. Then map a regular persistent class to this view (see How to: Map a Database View to a Persistent Class).

Files to Review

Documentation

More Examples

Does this example address your development requirements/objectives?

(you will be redirected to DevExpress.com to submit your response)

Example Code

XPO/NET.Core/Blazor/AspNetCore.Module/Services/XpoDataStoreProxy.cs
C#
using DevExpress.Xpo; using DevExpress.Xpo.DB; using DevExpress.Xpo.Helpers; using DevExpress.Xpo.Metadata; namespace AspNetCore.Module.Services; public class XpoDataStoreProxy : IDataStoreAsync, ICommandChannel { private SimpleDataLayer legacyDataLayer; private IDataStore legacyDataStore; private SimpleDataLayer tempDataLayer; private IDataStore tempDataStore; private string[] tempDatabaseTables = new string[] { "ModuleInfo", "XPObjectType" }; private bool IsTempDatabaseTable(string tableName) { if(!string.IsNullOrEmpty(tableName)) { foreach(string currentTableName in tempDatabaseTables) { if(tableName.EndsWith(currentTableName)) { return true; } } } return false; } public void Initialize(XPDictionary dictionary, string legacyConnectionString, string tempConnectionString) { ReflectionDictionary legacyDictionary = new ReflectionDictionary(); ReflectionDictionary tempDictionary = new ReflectionDictionary(); foreach(XPClassInfo ci in dictionary.Classes) { if(!IsTempDatabaseTable(ci.TableName)) { legacyDictionary.QueryClassInfo(ci.ClassType); } else { tempDictionary.QueryClassInfo(ci.ClassType); } } legacyDataStore = XpoDefault.GetConnectionProvider(legacyConnectionString, AutoCreateOption.DatabaseAndSchema); legacyDataLayer = new SimpleDataLayer(legacyDictionary, legacyDataStore); tempDataStore = XpoDefault.GetConnectionProvider(tempConnectionString, AutoCreateOption.DatabaseAndSchema); tempDataLayer = new SimpleDataLayer(tempDictionary, tempDataStore); } public AutoCreateOption AutoCreateOption { get { return AutoCreateOption.DatabaseAndSchema; } } public ModificationResult ModifyData(params ModificationStatement[] dmlStatements) { List<ModificationStatement> legacyChanges = new List<ModificationStatement>(dmlStatements.Length); List<ModificationStatement> tempChanges = new List<ModificationStatement>(dmlStatements.Length); foreach(ModificationStatement stm in dmlStatements) { if(IsTempDatabaseTable(stm.Table.Name)) { tempChanges.Add(stm); } else { legacyChanges.Add(stm); } } List<ParameterValue> resultSet = new List<ParameterValue>(); if(legacyChanges.Count > 0) { resultSet.AddRange(legacyDataLayer.ModifyData(legacyChanges.ToArray()).Identities); } if(tempChanges.Count > 0) { resultSet.AddRange(tempDataLayer.ModifyData(tempChanges.ToArray()).Identities); } return new ModificationResult(resultSet); } public SelectedData SelectData(params SelectStatement[] selects) { var isExternals = selects.Select(stmt => IsTempDatabaseTable(stmt.Table.Name)).ToList(); List<SelectStatement> mainSelects = new List<SelectStatement>(selects.Length); List<SelectStatement> externalSelects = new List<SelectStatement>(selects.Length); for(int i = 0; i < isExternals.Count; ++i) { (isExternals[i] ? externalSelects : mainSelects).Add(selects[i]); } var externalResults = (externalSelects.Count == 0 ? Enumerable.Empty<SelectStatementResult>() : tempDataLayer.SelectData(externalSelects.ToArray()).ResultSet).GetEnumerator(); var mainResults = (mainSelects.Count == 0 ? Enumerable.Empty<SelectStatementResult>() : legacyDataLayer.SelectData(mainSelects.ToArray()).ResultSet).GetEnumerator(); SelectStatementResult[] results = new SelectStatementResult[isExternals.Count]; for(int i = 0; i < results.Length; ++i) { var enumerator = isExternals[i] ? externalResults : mainResults; enumerator.MoveNext(); results[i] = enumerator.Current; } return new SelectedData(results); } public UpdateSchemaResult UpdateSchema(bool dontCreateIfFirstTableNotExist, params DBTable[] tables) { List<DBTable> db1Tables = new List<DBTable>(); List<DBTable> db2Tables = new List<DBTable>(); foreach(DBTable table in tables) { if(!IsTempDatabaseTable(table.Name)) { db1Tables.Add(table); } else { db2Tables.Add(table); } } legacyDataStore.UpdateSchema(false, db1Tables.ToArray()); tempDataStore.UpdateSchema(false, db2Tables.ToArray()); return UpdateSchemaResult.SchemaExists; } public object Do(string command, object args) { return ((ICommandChannel)legacyDataLayer).Do(command, args); } public Task<SelectedData> SelectDataAsync(CancellationToken cancellationToken, params SelectStatement[] selects) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(SelectData(selects)); } public Task<ModificationResult> ModifyDataAsync(CancellationToken cancellationToken, params ModificationStatement[] dmlStatements) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(ModifyData(dmlStatements)); } public Task<UpdateSchemaResult> UpdateSchemaAsync(CancellationToken cancellationToken, bool doNotCreateIfFirstTableNotExist, params DBTable[] tables) { cancellationToken.ThrowIfCancellationRequested(); return Task.FromResult(UpdateSchema(doNotCreateIfFirstTableNotExist, tables)); } }
XPO/NET.Core/Blazor/AspNetCore.Module/Services/XpoDataStoreProxyProvider.cs
C#
using DevExpress.Xpo.Metadata; using DevExpress.ExpressApp.Xpo; namespace AspNetCore.Module.Services; public class XpoDataStoreProxyProvider : IXpoDataStoreProvider { private XpoDataStoreProxy proxy; public XpoDataStoreProxyProvider() { proxy = new XpoDataStoreProxy(); } public DevExpress.Xpo.DB.IDataStore CreateUpdatingStore(bool allowUpdateSchema, out IDisposable[] disposableObjects) { disposableObjects = null; return proxy; } public DevExpress.Xpo.DB.IDataStore CreateWorkingStore(out IDisposable[] disposableObjects) { disposableObjects = null; return proxy; } public DevExpress.Xpo.DB.IDataStore CreateSchemaCheckingStore(out IDisposable[] disposableObjects) { disposableObjects = null; return proxy; } public XPDictionary XPDictionary { get { return null; } } public string ConnectionString { get { return null; } } public bool IsInitialized { get; private set; } public void Initialize(XPDictionary dictionary, string legacyConnectionString, string tempConnectionString) { proxy.Initialize(dictionary, legacyConnectionString, tempConnectionString); IsInitialized = true; } }
XPO/NET.Core/Blazor/AspNetCore.Blazor.Server/Startup.cs
C#
using DevExpress.ExpressApp.ApplicationBuilder; using DevExpress.ExpressApp.Blazor.ApplicationBuilder; using DevExpress.ExpressApp.Blazor.Services; using DevExpress.Persistent.Base; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Components.Server.Circuits; using DevExpress.ExpressApp.Xpo; using AspNetCore.Blazor.Server.Services; using DevExpress.ExpressApp.Core; using AspNetCore.Module.Services; namespace AspNetCore.Blazor.Server; public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) { services.AddSingleton(typeof(Microsoft.AspNetCore.SignalR.HubConnectionHandler<>), typeof(ProxyHubConnectionHandler<>)); services.AddRazorPages(); services.AddServerSideBlazor(); services.AddHttpContextAccessor(); services.AddSingleton<XpoDataStoreProxyProvider>(); services.AddScoped<CircuitHandler, CircuitHandlerProxy>(); services.AddXaf(Configuration, builder => { builder.UseApplication<AspNetCoreBlazorApplication>(); builder.Modules .Add<AspNetCore.Module.AspNetCoreModule>(); builder.ObjectSpaceProviders .Add(serviceProvider => new XPObjectSpaceProvider(serviceProvider.GetRequiredService<XpoDataStoreProxyProvider>())) .AddNonPersistent(); }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if(env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. To change this for production scenarios, see: https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseRequestLocalization(); app.UseStaticFiles(); app.UseRouting(); app.UseXaf(); app.UseEndpoints(endpoints => { endpoints.MapXafEndpoints(); endpoints.MapBlazorHub(); endpoints.MapFallbackToPage("/_Host"); endpoints.MapControllers(); }); } }
XPO/NET.Framework/WinWebSolution.Module/Module.cs
C#
using System; using System.Collections.Generic; using DevExpress.ExpressApp; using System.Reflection; using DevExpress.ExpressApp.Xpo; using System.Configuration; namespace WinWebSolution.Module { public sealed partial class WinWebSolutionModule : ModuleBase { private static XpoDataStoreProxyProvider provider; public WinWebSolutionModule() { InitializeComponent(); } public override void Setup(XafApplication application) { base.Setup(application); application.CustomCheckCompatibility += new EventHandler<CustomCheckCompatibilityEventArgs>(application_CustomCheckCompatibility); application.CreateCustomObjectSpaceProvider += new EventHandler<CreateCustomObjectSpaceProviderEventArgs>(application_CreateCustomObjectSpaceProvider); } void application_CreateCustomObjectSpaceProvider(object sender, CreateCustomObjectSpaceProviderEventArgs e) { if(provider == null) { provider = new XpoDataStoreProxyProvider(); } e.ObjectSpaceProvider = new XPObjectSpaceProvider(provider); } void application_CustomCheckCompatibility(object sender, CustomCheckCompatibilityEventArgs e) { if(provider != null && !provider.IsInitialized) { provider.Initialize(((XPObjectSpaceProvider)e.ObjectSpaceProvider).XPDictionary, ConfigurationManager.ConnectionStrings["LegacyDatabaseConnectionString"].ConnectionString, ConfigurationManager.ConnectionStrings["TempDatabaseConnectionString"].ConnectionString); } } } }

Disclaimer: The information provided on DevExpress.com and affiliated web properties (including the DevExpress Support Center) is provided "as is" without warranty of any kind. Developer Express Inc disclaims all warranties, either express or implied, including the warranties of merchantability and fitness for a particular purpose. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.

Confidential Information: Developer Express Inc does not wish to receive, will not act to procure, nor will it solicit, confidential or proprietary materials and information from you through the DevExpress Support Center or its web properties. Any and all materials or information divulged during chats, email communications, online discussions, Support Center tickets, or made available to Developer Express Inc in any manner will be deemed NOT to be confidential by Developer Express Inc. Please refer to the DevExpress.com Website Terms of Use for more information in this regard.