DEV Community

Cover image for Kentico Xperience 13 Beta 3 - New Data Access APIs
Sean G. Wright
Sean G. Wright

Posted on • Edited on

Kentico Xperience 13 Beta 3 - New Data Access APIs

The Kentico Xperience 13 Betas let us, the development community, get access to the latest upcoming features in the next version of Kentico Xperience.

Beta 3 is now available. So let's dive in ๐Ÿคฝ๐Ÿพโ€โ™€๏ธ!


How to Get the Kentico Xperience 13 Beta

To get access to the beta we need to have a DevNet account. If you don't have one, you can register here.

After logging into DevNet with our account, we can download the beta in the downloads section of the site.

The beta has instructions ๐Ÿ“ƒ on how to get it up and running, what features are new in this version, the list of known issues, and what the focus should be for developers and users of Kentico Xperience when trying out this version of the beta.


Data Access in Kentico Xperience 13

There are many new cool ๐Ÿ˜Ž features and changes coming in Kentico Xperience 13, and I've already covered the support for ASP.NET Core and some of the new Page Builder architecture when targeting .NET Core...

As excited ๐Ÿ˜ƒ as I am to be finally building Kentico Xperience sites with ASP.NET Core, I don't want to overlook some of the biggest low-level improvements to Xperience - changes to DataQueryBase/ObjectQueryBase and Xperience's data access APIs.

Interfaces vs Static Methods

Up until now, the standard pattern for accessing strongly typed data in Xperience has been to use the various *InfoProvider class static methods ๐Ÿง.

For example, to get the collection of all Sites running in the application we would write the following:

IEnumerable<SiteInfo> sites = SiteInfoProvider
    .GetSites()
    .AsEnumerable();

foreach (SiteInfo site in sites)
{
    Debug.WriteLine(site.SiteName);
}
Enter fullscreen mode Exit fullscreen mode

The static nature of GetSites() means we don't have to instantiate a SiteInfoProvider to query the database.

This means we can query it wherever we want ๐Ÿ˜‰!

Much wow, such ease!

This was great in a WebForms world where object construction often relied on factory patterns and data access code was commonly found directly in the presentation layer (.aspx.cs and .ascx.cs files).

In ASP.NET Core, Dependency Injection is the law of the land.

The framework is designed to be testable, pushing volatile dependencies to outside of the application and instead relying on abstractions supplied via constructor parameters ๐Ÿ’ช๐Ÿฝ.

To accommodate this architectural shift, Xperience has changed the way data is accessed:

IEnumerable<SiteInfo> sites = SiteInfo
    .Provider
    .Get()
    .AsEnumerable();

foreach (SiteInfo site in sites)
{
    Debug.WriteLine(site.SiteName);
}
Enter fullscreen mode Exit fullscreen mode

This looks very similar, but there is a key different - we are using the .Provider property on SiteInfo ๐Ÿคจ.

This property is typed as:

public interface ISiteInfoProvider : 
    IInfoProvider<SiteInfo>, 
    IInfoByIdProvider<SiteInfo>, 
    IInfoByGuidProvider<SiteInfo>, 
    IInfoByNameProvider<SiteInfo>
Enter fullscreen mode Exit fullscreen mode

And, just for reference, IInfoProvider<TInfo> is defined as:

public interface IInfoProvider<TInfo> 
    where TInfo : AbstractInfoBase<TInfo>, new()
{   
    void Delete(TInfo info);
    ObjectQuery<TInfo> Get();
    void Set(TInfo info);
}
Enter fullscreen mode Exit fullscreen mode

This means Kentico is exposing abstractions, like ISiteInfoProvider, to give us access to data querying, instead of concrete or static types ๐Ÿค”.

When using these APIs in places where Dependency Injection is not available, our code won't look much different (though Unit Testing might be easier!).

However, when we need to perform data access in parts of the application where Dependency Injection is supported and configured, we can rely on the abstractions for querying.

The code below, if run in an ASP.NET Core app, doesn't require us to register the ISiteInfoProvider type with the Dependency Injection container - Xperience handles that for us ๐Ÿ‘๐Ÿพ:

public class SiteController
{
    private readonly ISiteInfoProvider provider;

    public SiteController(ISiteInfoProvider provider) =>
        this.provider = provider;

    public IActionResult GetSites()
    {
        var sites = provider.Get().AsEnumerable();

        foreach (SiteInfo site in sites)
        {
            // ...
        }

        return View(...);
    }
}
Enter fullscreen mode Exit fullscreen mode

This code is far easier to test and requires fewer of Xperience's unit testing hacks ... I mean, APIs ๐Ÿ˜…, compared to using the static *InfoProvider methods.

It should be noted that most of the CRUD (Create, Read, Update, Delete) static methods have been marked as Obsolete ๐Ÿ˜ฎ and it is recommended to use the new *Info.Provider.Get/Set/Delete methods instead.

New DataQueryBase/ObjectQueryBase Methods

In addition to changing how we perform queries against the database, Xperience has also added new ways to get the results of those queries.

In previous versions, to force evaluation of the ObjectQuery<T> being built up, we would either start iterating over the query or call .Result, .TypedResult, or .ToList() to force an evaluation.

Since ObjectQueryBase implements IEnumerable any query we build up, with all the various query extension methods, can be enumerated ๐Ÿค“, which forces the query to be executed and resulting DataSet be populated.

This approach was fine, but most of the time what we really wanted was to:

  • Force evaluation
  • Choose between typed/untyped results
  • Expose an IEnumerable since it's the simplest API for consumers
    • .Result returns a DataSet and .TypedResult returns an InfoDataSet<T> which has lots of properties we typically don't need ๐Ÿ˜‘

Xperience now gives us some convenient methods to cut down on boilerplate and give us what we want:

IEnumerable<IDataRecord> GetEnumerableResult();
IEnumerable<TInfo> GetEnumerableTypedResult();
Enter fullscreen mode Exit fullscreen mode

GetEnumerableResult() gives us access to the System.Data.IDataRecord which is a bit friendlier than trying to get access to, or work with DataRow:

// Before

IEnumerable<int> siteIds = SiteInfo.Provider.Get()
    // Forces immediate execution
    .Result
    .Tables[0]
    // Exposes an Enumerable
    .AsEnumerable()
    .Select(r => 
        ValidationHelper.GetInteger(r[nameof(SiteInfo.SiteID)], 0)
    );

// After

IEnumerable<int> siteIds = SiteInfo.Provider.Get()
    // Forces immediate execution and exposes and Enumerable
    .GetEnumerableResult() 
    .Select(r =>
        r.GetInt32(r.GetOrdinal(nameof(SiteInfo.SiteID)))
    );
Enter fullscreen mode Exit fullscreen mode

For the strongly typed variation, GetEnumerableTypedResult(), we get to clean up a few lines as well ๐Ÿ‘:

// Before

IEnumerable<SiteInfo> sites = SiteInfo.Provider.Get()
    // Forces immediate execution
    .TypedResult 
    // Exposes an Enumerable
    .AsEnumerable();

// After

IEnumerable<SiteInfo> sites = SiteInfo.Provider.Get()
    // Forces immediate execution and exposes and Enumerable
    .GetEnumerableTypedResult();
Enter fullscreen mode Exit fullscreen mode

These changes are small, but I like them because they improve the developer experience ๐Ÿค— and encourage consistent patterns.

Async Querying!

Update: 2020-08-13

I should have double checked and tested my code ๐Ÿคฆโ€โ™‚๏ธ.
While the async querying works fine for ObjectQuery, it throws exceptions when used with DocumentQuery.
This is even noted in the comment for the method on DocumentQueryBase<T>:

// The method is not supported for document query and always throws a System.NotSupportedException.

I have contacted Kentico support about this, as I think it's a bad design decision to add a base class method that always throws an exception in a child class.

I was told the team is working on getting async querying implemented by the time of Xperience 13's release.

Let's hope they get it done ๐Ÿคž๐Ÿคž!

Back when Web Forms was developed, concurrency on the web wasn't a primary concern, but 10 years later, when async/await was introduced in C# 5, concurrent programming had become the best way to get web applications to scale.

Kentico Xperience's data access APIs were built for the world of Web Forms, so they always lacked Async integration and would block the HTTP request thread when querying the database ๐Ÿ™.

The lack of Async data access has been resolved in Xperience 13 and we can now use async/await all the way from our Controller Actions to the database call and back ๐Ÿ˜.

This is how I feel when writing Xperience async queries

The two APIs we care most about are defined on DataQueryBase/ObjectQueryBase and since DocumentQueryBase inherits ObjectQueryBase and DataQueryBase, we can Async query for documents as well:

.GetEnumerableResultAsync();
.GetEnumerableTypedResultAsync();
Enter fullscreen mode Exit fullscreen mode

These are the Async equivalents of the methods we looked at already.

So what does Async querying look like in Xperience 13?

public class SiteController
{
    private readonly ISiteInfoProvider provider;

    public SiteController(ISiteInfoProvider provider) =>
        this.provider = provider;

    public async Task<IActionResult> GetSites()
    {
        var sites = await provider
            .Get()
            .GetEnumerableTypedResultAsync();

        // ...

        return View(...);
    }
}
Enter fullscreen mode Exit fullscreen mode

Picard clapping

Providers as Repositories

With the new IInfoProvider<TInfo> (and related interfaces) APIs, we now have a consistent set of methods we use for query, independent of the type:

Intellisense list of ISiteInfoProvider methods

We no longer have to use SiteInfoProvider.GetSites() for sites but UserInfoProvider.GetUsers() for users.

Instead, both look like this:

SiteInfo.Provider.Get();
UserInfo.Provider.Get();
Enter fullscreen mode Exit fullscreen mode

Here's a list of all the new, standard, methods:

ObjectQuery<TInfo> Get();
TInfo Get(int id);
TInfo Get(string name);
TInfo Get(Guid guid);

Task<TInfo> GetAsync(int id, CancellationToken token);
Task<TInfo> GetAsync(string name, CancellationToken token);
Task<TInfo> GetAsync(Guid guid, CancellationToken token);

void Delete(TInfo info);
void Set(TInfo info); // handles both create and update
Enter fullscreen mode Exit fullscreen mode

There's an implication here that might not be immediately obvious... ๐Ÿค”

Xperience is now giving us a set of Repositories through a generic interface, instead of a bunch of data access initialization points.

Generic interfaces are great opportunities for cross-cutting concerns through Decoration.

I've covered some powerful ways to use Decoration with Xperience in my Design Patterns blog series, and I'm a huge proponent of the pattern for any cross-cutting, like logging or caching โœ”.

There are currently some technical limitations around decorating Xperience's built-in types due to some uses of type casting internally, but this could be fixed in the future.

Another implication of this repository pattern is that we don't need to make another layer of repositories in our applications.

If you find you want to encapsulate common read/write scenarios, try the Command / Query Pattern instead.

My Design Pattern posts I linked above also cover the topic of Command / Query vs Repository ๐Ÿ˜‰.


Xperience hasn't (as of yet) updated all the data access APIs, and some are still the same as before:

// No Async, no new methods
int settingValue = SettingsKeyInfoProvider.GetIntValue("keyName");
Enter fullscreen mode Exit fullscreen mode

But that doesn't mean we can't opt-into the new awesomeness seen elsewhere, if needed:

SettingsKeyInfo setting = (await SettingsKeyInfoProvider
    .GetSettingsKeys()
    .WhereEquals(nameof(SettingsKeyInfo.KeyName), "someSetting")
    .TopN(1)
    .GetEnumerableTypedResultAsync())
    .FirstOrDefault()
Enter fullscreen mode Exit fullscreen mode

Conclusion

Kentico Xperience has always made it easy for developers to work with data created by the application and stored in the database.

We've been using the same powerful and flexible data access APIs for several years now.

With Xperience 13 we are gaining access to new ways of querying for data, including:

  • Interfaces instead of static methods
  • New DataQueryBase/ObjectQueryBase methods to clean up boilerplate
  • Async querying
  • Standardized interfaces and methods following a repository pattern

All of these new features are opt-in, though with some existing APIs being deprecated we might want to opt-in a little more aggressively for some of them.

When using Xperience 13 with ASP.NET Core, we will see the full benefit of these changes with Dependency Injection of providers and concurrent requests using async/await.

I find all of this very exciting (yes, I'm excited by new APIs ๐Ÿคทโ€โ™‚๏ธ), and I'd love to hear your thoughts on how you might leverage the new features coming in Xperience 13.

As always, thanks for reading ๐Ÿ™!


We've put together a list over on Kentico's GitHub account of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:

#kentico

Or my Kentico blog series:

Top comments (2)

Collapse
 
hades200082 profile image
Lee Conlin

Very interesting read. Can ObjectQuery GetEnumerableTypedResultAsync and GetEnumerableTypedResult be mocked with moq?

Collapse
 
seangwright profile image
Sean G. Wright

Lee,

The methods on ObjectQueryBase<T> are virtual, so assuming you somehow mock that, you should be able to override the methods.

The new Get() methods on IInfoProvider<T> are also virtual, so by mocking those and having them return a mocked ObjectQuery<T> (which inherits ObjectQueryBase<T>) you should be able to fake out the whole data access layer.

That said, I don't often see this kind of code being mocked because the internals of the querying code are very specific to getting data out of the database, and this seems like a much better opportunity for integration tests.

By putting code that interacts with DocumentQuery<T> and ObjectQuery<T> behind abstractions you can unit test your business logic, and integration test your data access implementation.