I got some questions last week about where to find the webinar video "Object-Relational Mapping for .NET Core with DevExpress XPO" I did as kick-off on our v17.2 release webinar series. Well, the reason it took some time is because we prepared it to be hosted on the Microsoft Channel 9 site.
To show how cross-platform XPO and .NET Core 2 are, I decided to use a Linux Mint environment in the first part, Windows 10 in the second part and Mac OS in the third (Xamarin) part.
I didn't even mention in the webinar that I was running my MS-SQL Server instance in a Docker container on my Asustor NAS Device (which is Linux based), so it was cross-platform all the way!
Webinar outline
After the general introduction on Object Relational Mapping, the coding part took of. I started with a small console application to show the absolute basics of XPO.
Connecting to the datastore
The first step is to connect to a datastore which is done throught the DataLayer. XPO has several difference DataLayer implementations for different kinds of applications.
In the console application, we can use the simple DataLayer and initialize it like this:
XpoDefault.DataLayer = XpoDefault.GetDataLayer( SQLiteConnectionProvider.GetConnectionString("console.db"), AutoCreateOption.DatabaseAndSchema);
In the Web API example, there was some more code involved for initializing the DataLayer. Because we're dealing with a multi-threaded web application, we want to initialize a singleton instance of a ThreadSafeDatalayer. Besides that, we also want to setup a database connection pool to make the app as performant as possible:
string pooledConnectionString = XpoDefault.GetConnectionPoolString(connectionString); var dataStore = XpoDefault.GetConnectionProvider(pooledConnectionString, AutoCreateOption.SchemaAlreadyExists); var dataLayer = new ThreadSafeDataLayer(dictionary, dataStore); ; return dataLayer;
With some extension methods, I'm using the .NET Core dependency injection to create that singleton instance, and I inject a UnitOfWork into any Web API Controller which has a constructor with a parameter of type UnitOfWork:
public static class CustomXpoExtensions { public static IServiceCollection AddXpoPooledDataLayer(this IServiceCollection serviceCollection, string connectionString) { return serviceCollection.AddSingleton(XpoHelper.CreatePooledDataLayer(connectionString)); } public static IServiceCollection AddXpoDefaultUnitOfWork(this IServiceCollection serviceCollection) { return serviceCollection.AddScoped((sp) => new UnitOfWork()); } public static IServiceCollection AddXpoUnitOfWork(this IServiceCollection serviceCollection) { return serviceCollection.AddScoped((sp) => new UnitOfWork(sp.GetService())); } public static IApplicationBuilder UseXpoDemoData(this IApplicationBuilder app) { using(var scope = app.ApplicationServices.GetService().CreateScope()) { XpoHelper.CreateDemoData(() => scope.ServiceProvider.GetService()); } return app; } }
We can use these extension methods in the Startup.cs like this:
public void ConfigureServices(IServiceCollection services) { services .AddXpoPooledDataLayer(MSSqlConnectionProvider.GetConnectionString("sql-server-ip", "user", "test123", "WebAppDemo")) .AddXpoUnitOfWork() .AddMvc() .AddJsonOptions(options => { // use the custom resolver (above) options.SerializerSettings.ContractResolver = new XpoContractResolver(); // don't kill yourself over loop properties (probably not really needed now, I // inserted this originally to work around the This property on the XPO types) options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); }
The AddJsonOptions in the above code adds a custom Json serializer to the App to make sure certain fields of our XPO entities are not being serialized to Json. This fields are part of the infrastructure or XPO.
For the Xamarin example, I used again a ThreadSafe Datalayer because a Xamarin app will be multi-threaded as well.
Defining entities
In the examples, I used an Order type together with the OrderItem type. This gave me the opportunity to show how to setup a Master-Detail relation ship as well.
The first thing you see is that I derived my entities from the XPO based type XPObject.
public class Order : XPObject { public Order(Session session) : base(session) { } //... code omitted }
The reason for using this base type is that several nice features of XPO will be made available then like "Change Notifications". This behaviour comes in quite handy when doing some caching to get the most out of your application.
Please note that the XPObject requires you to supply a Session (or UnitOfWork) to its constructor. This is necessary to let the infrastructure of XPO dot its thing with the object being instantiated.
In case you have an existing class hierarchy that you want to persist to a database and you don't want to use constructor and Sessions etc. in there, you can also work with plain simple objects (POCO) which is similar as with Entity Framework.
An example of this is the Xamarin demo which works with the POCO objects created by the project template.
public class Item { public string Id { get; set; } public string Text { get; set; } public string Description { get; set; } } using (var uow = XpoHelper.CreateUnitOfWork()) { item.Id = Guid.NewGuid().ToString(); // I'm using the Save(..) method which receives the POCO we want to persist uow.Save(item); uow.CommitChanges(); }
Another interesting thing is the implementation of properties:
public class Order : XPObject { //... code omitted public Order(Session session) : base(session) { } private DateTime _OrderDate; public DateTime OrderDate { get { return _OrderDate; } set { SetPropertyValue("OrderDate", ref _OrderDate, value); } } }
This SetPropertyValue(..) method is one of the methods made available through the XPObject and deals with the Change Notifications.
Defining relationships between entities is pretty straight forward. In a master-detail relationship, the master entity (Order) has a collection of detail entities (OrderItem), while the detail entity holds a single reference to its master entity like shown below:
public class Order : XPObject { public Order(Session session) : base(session) { } //... code omitted [Association("Order-OrderItems"), Aggregated] public XPCollection<OrderItem> OrderItems { get { return GetCollection<OrderItem>("OrderItems"); } } } public class OrderItem : XPObject { public OrderItem(Session session) : base(session) { } //... code omitted private Order _Order; [Association("Order-OrderItems")] public Order Order { get { return _Order; } set { SetPropertyValue("Order", ref _Order, value); } } }
Also note the Association and Aggregated attributes. They tell XPO what kind of relation we are trying to setup.
Another powerful feature that comes with XPO is the use of the PersistentAlias attribute. With this you can specify that certain (readonly) fields need to be determined/evaluated by the database system being used.
I used 2 examples in the project, one which calculated the total price per order item, which is Qty * UnitPrice:
public class OrderItem : XPObject { //... code omitted public int Qty { //... code omitted } public decimal UnitPrice{ //... code omitted } [PersistentAlias("Qty * UnitPrice")] public decimal Price { get { return Convert.ToDecimal(EvaluateAlias("Price")); } } }
The second one is doing an aggregated calculation as part of the Order entity which sums the Alias from the detail set OrderItems:
public class Order : XPObject { //... code omitted [Association("Order-OrderItems"), Aggregated] public XPCollection OrderItems { get { return GetCollection("OrderItems"); } } [PersistentAlias("OrderItems.Sum(Price)")] public decimal TotalAmount { get { return Convert.ToDecimal(EvaluateAlias("TotalAmount")); } } }
Please not that the syntax being used is part of our database agnostic criteria language. More about it can be found here.
Querying the datastore
There are several ways of querying the datastore. In the examples I've used the Linq querying mechanism. This query is initiated through the Session.Query<T>() method:
using (var uow = new UnitOfWork()) { var orders = from o in uow.Query<Order>() where o.OrderDate < DateTime.Now.AddDays(-10) orderby o.OrderDate select o; foreach (var o in orders) { Console.WriteLine($"Order #{o.OrderNo} / {o.OrderDate}, client {o.Client}, Total Amount { o.TotalAmount }"); foreach(var i in o.OrderItems) { Console.WriteLine($" {i.Qty} x {i.Description} ({i.UnitPrice}) = {i.Price}"); } } }
XPO also supports another query mechanism by using the CriteriaOperator classes. This are super-powerful and they also allow you to select programmatically. This tends to be a problematic with Linq.
XPO CriteriaOperators allow you to create a very complex filtering clause which will be transformed to the SQL WHERE clause when executed.
I could rewrite the above code fragement like this:
using (var uow = new UnitOfWork()) { XPCollection<Order> orders = new XPCollection<Order>(uow, new BinaryOperator("OrderDate", DateTime.Now.AddDays(-10), BinaryOperatorType.Less), new SortProperty("OrderDate", SortingDirection.Ascending)); }
Or by using the CriteriaOperator.Parse(...) construct:
I could rewrite the above code fragement like this:
using (var uow = new UnitOfWork()) { XPCollection<Order> orders = new XPCollection<Order>(uow, CriteriaOperator.Parse("OrderDate < ?", DateTime.Now.AddDays(-10)), new SortProperty("OrderDate", SortingDirection.Ascending)); }
Do note the '?' which is used as a parameter indicator in the query language.
More on the XPO Criteria Language can be found here.
This Webinar was only short introduction since XPO involves much more. What to think about inheritance, interface support, a whole set of Attributes to decorate your classes and properties with, support for Views and StoredProcedures.
I even didn't mention our Visual Designer which comes with VisualStudio as well as our XPO Profiler tool to analyze which SQL is being fired against the database:
If you want to play around with the samples I did in the webinar, check out my Github repo at: https://github.com/donwibier/DXXPOCore.
Do note that if you clone the repo, you will need to have your personal DevExpress NuGet feed configured like instructed here.
Let me know if you want to know more!