DataObjects.Net Extensions are small projects that extend standard functionality of DataObjects.Net core. They are maintained by Xtensive engineers and volunteers from DataObjects.Net community. Each extension has a corresponding NuGet package so they can be installed separately or in any combination. Using NuGet is a recommended way of installing the extensions.
Starting from v6.0 the extensions’ source code is available on GitHub as part of main repository and binaries are published to NuGet package library.
The extension provides a set of IQueryable extension methods that are translated to server-side UPDATE or DELETE commands.
Add Bulk Operations package to your project.
//sync
Query.All<Bar>()
.Where(a => a.Id == 1)
.Set(a => a.Count, 2)
.Update();
//async
await Query.All<Bar>()
.Where(a => a.Id == 1)
.Set(a => a.Count, 2)
.UpdateAsync();
//sync
Query.All<Bar>()
.Where(a => a.Id==1)
.Set(a => a.Count, a => a.Description.Length)
.Update();
//async
await Query.All<Bar>()
.Where(a => a.Id==1)
.Set(a => a.Count, a => a.Description.Length)
.UpdateAsync();
// Emulating entity loading
var bar = Query.Single<Bar>(1);
//sync
Query.All<Foo>()
.Where(a => a.Id == 2)
.Set(a => a.Bar, bar)
.Update();
// Emulating entity loading
var bar = Query.Single<Bar>(1);
//async
await Query.All<Foo>()
.Where(a => a.Id == 2)
.Set(a => a.Bar, bar)
.UpdateAsync();
//sync
Query.All<Foo>()
.Where(a => a.Id == 1)
.Set(a => a.Bar, a => Query.Single<Bar>(1))
.Update();
//async
await Query.All<Foo>()
.Where(a => a.Id == 1)
.Set(a => a.Bar, a => Query.Single<Bar>(1))
.UpdateAsync();
//sync
Query.All<Foo>()
.Where(a => a.Id == 1)
.Set(a => a.Bar, a => Query.All<Bar>().Single(b => b.Name == "test"))
.Update();
//async
await Query.All<Foo>()
.Where(a => a.Id == 1)
.Set(a => a.Bar, a => Query.All<Bar>().Single(b => b.Name == "test"))
.UpdateAsync();
//sync
bool condition = CheckCondition();
var query = Query.All()<Bar>
.Where(a => a.Id == 1)
.Set(a => a.Count, 2);
if(condition)
query = query.Set(a => a.Name, a => a.Name + "test");
query.Update();
//async
bool condition = CheckCondition();
var query = Query.All()<Bar>
.Where(a => a.Id == 1)
.Set(a => a.Count, 2);
if(condition)
query = query.Set(a => a.Name, a => a.Name + "test");
await query.UpdateAsync();
//sync
Query.All<Bar>()
.Where(a => a.Id == 1)
.Update(a => new Bar(null) { Count = 2, Name = a.Name + "test", dozens of other properties... });
//async
await Query.All<Bar>()
.Where(a => a.Id == 1)
.UpdateAsync(a => new Bar(null) { Count = 2, Name = a.Name + "test", dozens of other properties... });
//sync
Query.All<Foo>()
.Where(a => a.Id == 1)
.Delete();
//async
await Query.All<Foo>()
.Where(a => a.Id == 1)
.DeleteAsync();
The extension transparently solves a task of application or service localization. This implies that localizable resources are a part of domain model so they are stored in database.
<Xtensive.Orm>
<domains>
<domain ... >
<types>
<add assembly="your assembly"/>
<add assembly="Xtensive.Orm.Localization"/>
</types>
</domain>
</domains>
</Xtensive.Orm>
or in code
domainConfiguration.Types.Register(typeof(ILocalizable<>).Assembly);
[HierarchyRoot]
public class Page : Entity, ILocalizable<PageLocalization>
{
[Field, Key]
public int Id { get; private set; }
// Localizable field. Note that it is non-persistent
public string Title
{
get { return Localizations.Current.Title; }
set { Localizations.Current.Title = value; }
}
[Field] // This is a storage of all localizations for Page class
public LocalizationSet<PageLocalization> Localizations { get; private set; }
public Page(Session session) : base(session) {}
}
[HierarchyRoot]
public class PageLocalization : Localization<Page>
{
[Field(Length = 100)]
public string Title { get; set; }
public PageLocalization(Session session, CultureInfo culture, Page target)
: base(session, culture, target) {}
}
page.Title = "Welcome";
string title = page.Title;
var en = new CultureInfo("en-US");
var sp = new CultureInfo("es-ES");
var page = new Page(session);
page.Localizations[en].Title = "Welcome";
page.Localizations[sp].Title = "Bienvenido";
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
string title = page.Title; // title is "Welcome"
Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
string title = page.Title; // title is "Bienvenido"
using (new LocalizationScope(new CultureInfo("en-US"))) {
string title = page.Title; // title is "Welcome"
}
using (new LocalizationScope(new CultureInfo("es-ES"))) {
string title = page.Title; // title is "Bienvenido"
}
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
var query = from p in session.Query.All<Page>()
where p.Title=="Welcome"
select p;
Assert.AreEqual(1, query.Count());
Thread.CurrentThread.CurrentCulture = new CultureInfo("es-ES");
var query = from p in session.Query.All<Page>()
where p.Title=="Bienvenido"
select p;
Assert.AreEqual(1, query.Count());
Following examples show different ways to configure extension in configuration files of various types.
Example #1 Confugure default culture in App.config/Web.config
<configuration>
<configSections>
<section name="Xtensive.Orm" type="Xtensive.Orm.Configuration.Elements.ConfigurationSection, Xtensive.Orm"/>
<section name="Xtensive.Orm.Localization" type="Xtensive.Orm.Localization.Configuration.ConfigurationSection, Xtensive.Orm.Localization"/>
</configSections>
<Xtensive.Orm>
<domain ... >
<types>
<add assembly="your assembly"/>
<add assembly="Xtensive.Orm.Localization"/>
</types>
</domain>
</Xtensive.Orm>
<Xtensive.Orm.Localization>
<defaultCulture name="es-ES"/>
</Xtensive.Orm.Localization>
</configuration>
Such configuration is usually read with System.Configuration.ConfigurationManager. If project still supports such configurations then Localization configuration will be read automatically when it needs to be read. Sometimes a work-around is needed to read such configuration, for more read Example #2 and Example #3
Example #2 Reading old-style configuration of an assembly in NET 5 and newer.
Due to new architecture without AppDomain (which among the other things was in charge of gathering configuration files of loaded assemblies as it would be one configuration file) System.Configuration.ConfigurationManager now reads only configuration file of actual executable, loaded assemblies’ configuration files stay unreachable by default, though there is need to read some data from them. A great example is test projects which are usually get loaded by test runner executable, and the only configuration accessible in this case is test runner one.
Extra step is required to read configuration files in such cases. Thankfully, ConfigurationManager has methods to get access to assemblies’ configuration files.
To get access to an assembly configuration file it should be opened explicitly by
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
The instance returned from OpenExeConfiguration provides access to sections of the assembly configuration. DataObjects.Net configurations (DomainConfiguration, LocalizationConfiguration, etc.) have Load() methods that can recieve this instance. LocalizationConfiguration can be read like so
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
var localizationConfig = LocalizationConfiguration.Load(configuration);
// loaded configuration should be manually placed to
domainConfiguration.ExtensionConfigurations.Set(localizationConfig);
The domainConfiguration.ExtensionConfigurations is a new unified place from which the extension will try to get its configuration instead of calling default parameterless Load() method, which has not a lot of sense now, though the method is kept as a second source for backwards compatibility.
For more convenience, DomainConfiguration extensions are provided, which make code neater. For instance,
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
var domainConfiguration = DomainConfiguration.Load(configuration);
// the extension hides getting configuration with LocalizationConfiguration.Load(configuration)
// and also putting it to ExtensionConfigurations collection.
domainConfiguration.ConfigureLocalizationExtension(configuration);
Custom section names are also supported if for some reason default section name is not used.
Example #3 Reading old-style configuration of an assembly in a project that uses appsettings.json file.
If for some reason there is need to keep the old-style configuration then there is a work-around as well. Static configuration manager provides method OpenMappedExeConfiguration() which allows to get any *.config file as System.Configuration.Configuration instance. For example,
ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
configFileMap.ExeConfigFilename = "Orm.config"; //or other file name, the file should exist bin folder
var configuration = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
After that, as in previous example, the instance can be passed to Load method of LocalizationConfiguration to read extension configuration and later put it to DomainConfiguration.ExtensionConfigurations
var localizationConfiguration = LocalizationConfiguration.Load(configuration);
domainConfiguration.ExtensionConfigurations.Set(localizationConfiguration);
Extension usage will look like
domainConfiguration.ConfigureLocalizationExtension(configuration);
Example #4 Configure using Microsoft.Extensions.Configuration API.
This API allows to have configurations in various forms including JSON and XML formats. Loading of such files may differ depending on .NET version, check Microsoft manuals for instructions.
Allowed JSON and XML configuration definition look like below
<configuration>
<Xtensive.Orm.Localization>
<DefaultCulture>es-ES</DefaultCulture>
</Xtensive.Orm.Localization>
</configuration>
The API has certain issues with XML elements with attributes so it is recommended to use more up-to-date attributeless nodes as in the example. For JSON it is pretty clear, almost averyone knows its format.
LocalizationConfiguration.Load method can accept different types of abstractions from the API, including - Microsoft.Extensions.Configuration.IConfiguration, - Microsoft.Extensions.Configuration.IConfigurationRoot, - Microsoft.Extensions.Configuration.IConfigurationSection.
Loading of configuration may look like
var app = builder.Build();
//...
// tries to load from default section "Xtensive.Orm.Localization"
var localizationConfig = LocalizationConfiguration.Load(app.Configuration);
domainConfiguration.ExtensionConfigurations.Set(localizationConfig);
or, with use of extension, like
var app = builder.Build();
//...
// tries to load from default section "Xtensive.Orm.Localization"
// and additionally adds Xtensive.Orm.Localization assembly to domain types.
domainConfiguration.ConfigureLocalizationExtension(app.Configuration);
Example #5 Configure using Microsoft.Extensions.Configuration API from section with non-default name.
For configurations like
<configuration>
<Orm.Localization>
<DefaultCulture>es-ES</DefaultCulture>
</Orm.Localization>
</configuration>
{
"Orm.Localization": {
"DefaultCulture": "es-ES"
}
}
Loading of configuration may look like
var app = builder.Build();
//...
var localizationConfig = LocalizationConfiguration.Load(app.Configuration, "Orm.Localization");
domainConfiguration.ExtensionConfigurations.Set(localizationConfig);
or, with use of extension, like
var app = builder.Build();
//...
domainConfiguration.ConfigureLocalizationExtension(app.Configuration, "Orm.Localization");
Example #6 Configure using Microsoft.Extensions.Configuration API from sub-section deeper in section tree.
If for some reason extension configuration should be moved deeper in section tree like something below
<configuration>
<Orm.Extensions>
<Xtensive.Orm.Localization>
<DefaultCulture>es-ES</DefaultCulture>
</Xtensive.Orm.Localization>
</Orm.Extensions>
</configuration>
or in JSON
{
"Orm.Extensions": {
"Xtensive.Orm.Localization": {
"DefaultCulture": "es-ES"
}
}
}
Then section must be provided manually, code may look like
var app = builder.Build();
//...
var configurationRoot = app.Configuration;
var extensionsGroupSection = configurationRoot.GetSection("Orm.Extensions");
var localizationSection = extensionsGroupSection.GetSection("Xtensive.Orm.Localization");
var localizationConfig = LocalizationConfiguration.Load(localizationSection);
domainConfiguration.ExtensionConfigurations.Set(localizationConfig);
or, with use of extension method, like
var app = builder.Build();
//...
var configurationRoot = app.Configuration;
var extensionsGroupSection = configurationRoot.GetSection("Orm.Extensions");
var localizationSection = extensionsGroupSection.GetSection("Xtensive.Orm.Localization");
domainConfiguration.ConfigureLocalizationExtension(localizationSection);
The extension provides API for reprocessible operations. The reprocessible operation should represent a separate block of logic, usually a delegate of a method and be transactional.
Domain.Execute(session =>
{
// Task logic
});
Session is provided by reprocessing infrastructure. No session activation perfromed so use the instance you were gived by delegate.
2. Use WithSession to pass session from ouside, it this case infrastructure won’t open any session and uses yours instead. Don’t use session activation to pass session to delegate.
using (var externalSession = Domain.OpenSession()) {
Domain.WithSession(externalSession)
.Execute(session =>
{
// Task logic
});
}
To indicate that a particular strategy should be used, use the following syntax:
Domain.WithStrategy(new HandleReprocessExceptionStrategy())
.Execute(session =>
{
// Task logic
});
4. To omit setting up the strategy each time consider configuring it in application configuration file, e.g.:
<configSections>
...
<section name="Xtensive.Orm.Reprocessing"
type="Xtensive.Orm.Reprocessing.Configuration.ConfigurationSection, Xtensive.Orm.Reprocessing" />
</configSections>
<Xtensive.Orm.Reprocessing
defaultTransactionOpenMode="New"
defaultExecuteStrategy="Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing">
</Xtensive.Orm.Reprocessing>
Having that done, in scenarios with no strategy specified, the extension will automatically use the strategy from the configuration.
Such configuration as shown above is usually read with System.Configuration.ConfigurationManager. If project still supports such configurations then Reprocessing configuration will be read automatically when it needs to be read. Sometimes a work-around is needed to read such configuration.
Following examples show different ways to configure extension in configuration files of various types.
Example #1 Reading old-style configuration of an assembly in NET 5 and newer.
Due to new architecture without AppDomain (which among the other things was in charge of gathering configuration files of loaded assemblies as it would be one configuration file) System.Configuration.ConfigurationManager now reads only configuration file of actual executable, loaded assemblies’ configuration files stay unreachable by default, though there is need to read some data from them. A great example is test projects which are usually get loaded by test runner executable, and the only configuration accessible in this case is test runner one.
Extra step is required to read configuration files in such cases. Thankfully, ConfigurationManager has methods to get access to assemblies’ configuration files.
To get access to an assembly configuration file it should be opened explicitly by
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
The instance returned from OpenExeConfiguration provides access to sections of the assembly configuration. DataObjects.Net configurations (DomainConfiguration, ReprocessingConfiguration, etc.) have Load() methods that can recieve this instance. ReprocessingConfiguration can be read like so
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
var reprocessingConfig = ReprocessingConfiguration.Load(configuration);
// loaded configuration should be manually placed to
domainConfiguration.ExtensionConfigurations.Set(reprocessingConfig);
The domainConfiguration.ExtensionConfigurations is a new unified place from which the extension will try to get its configuration instead of calling default parameterless Load() method, which has not a lot of sense now, though the method is kept as a second source for backwards compatibility.
For more convenience, DomainConfiguration extensions are provided, which make code more neater. For instance,
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
// the extension hides getting configuration with ReprocessingConfiguration.Load(configuration)
// and also putting it to ExtensionConfigurations collection.
domainConfiguration.ConfigureReprocessingExtension(configuration);
Custom section names are also supported if for some reason default section name is not used.
Example #2 Reading old-style configuration of an assembly in a project that uses appsettings.json file.
If for some reason there is need to keep the old-style configuration then there is a work-around as well. Static configuration manager provides method OpenMappedExeConfiguration() which allows to get any *.config file as System.Configuration.Configuration instance. For example,
ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
configFileMap.ExeConfigFilename = "Orm.config"; //or other file name, the file should exist bin folder
var configuration = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
After that, as in previous example, the instance can be passed to Load method of ReprocessingConfiguration to read extension configuration and later put it to DomainConfiguration.ExtensionConfigurations
var reprocessingConfiguration = ReprocessingConfiguration.Load(configuration);
domainConfiguration.ExtensionConfigurations.Set(reprocessingConfiguration);
Extension usage will look like
domainConfiguration.ConfigureReprocessingExtension(configuration);
Example #3 Configure using Microsoft.Extensions.Configuration API.
This API allows to have configurations in various forms including JSON and XML formats. Loading of such files may differ depending on .NET version, check Microsoft manuals for instructions.
Allowed JSON and XML configuration definition look like below
<configuration>
<Xtensive.Orm.Reprocessing>
<DefaultTransactionOpenMode>New</DefaultTransactionOpenMode>
<DefaultExecuteStrategy>Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing</DefaultExecuteStrategy>
</Xtensive.Orm.Reprocessing>
</configuration>
{
"Xtensive.Orm.Reprocessing": {
"DefaultTransactionOpenMode" : "New",
"DefaultExecuteStrategy" : "Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing"
}
}
The API has certain issues with XML elements with attributes so it is recommended to use more up-to-date attributeless nodes. For JSON it is pretty clear, almost averyone knows its format.
ReprocessingConfiguration.Load method can accept different types of abstractions from the API, including - Microsoft.Extensions.Configuration.IConfiguration; - Microsoft.Extensions.Configuration.IConfigurationRoot; - Microsoft.Extensions.Configuration.IConfigurationSection.
Loading of configuration may look like
var app = builder.Build();
//...
// tries to load from default section "Xtensive.Orm.Reprocessing"
var reprocessingConfig = ReprocessingConfiguration.Load(app.Configuration);
domainConfiguration.ExtensionConfigurations.Set(reprocessingConfig);
or, with use of extension, like
var app = builder.Build();
//...
// tries to load from default section "Xtensive.Orm.Reprocessing"
// and put it into domainConfiguration.ExtensionConfigurations
domainConfiguration.ConfigureReprocessingExtension(app.Configuration);
Example #4 Configure using Microsoft.Extensions.Configuration API from section with non-default name.
For configurations like
<configuration>
<Orm.Reprocessing>
<DefaultTransactionOpenMode>New</DefaultTransactionOpenMode>
<DefaultExecuteStrategy>Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing</DefaultExecuteStrategy>
</Orm.Reprocessing>
</configuration>
{
"Orm.Reprocessing": {
"DefaultTransactionOpenMode" : "New",
"DefaultExecuteStrategy" : "Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing"
}
}
Loading of configuration may look like
var app = builder.Build();
//...
var reprocessingConfig = ReprocessingConfiguration.Load(app.Configuration, "Orm.Reprocessing");
domainConfiguration.ExtensionConfigurations.Set(reprocessingConfig);
or, with use of extension, like
var app = builder.Build();
//...
domainConfiguration.ConfigureReprocessingExtension(app.Configuration, "Orm.Reprocessing");
Example #5 Configure using Microsoft.Extensions.Configuration API from sub-section deeper in section tree.
If for some reason extension configuration should be moved deeper in section tree like something below
<configuration>
<Orm.Extensions>
<Xtensive.Orm.Reprocessing>
<DefaultTransactionOpenMode>New</DefaultTransactionOpenMode>
<DefaultExecuteStrategy>Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing</DefaultExecuteStrategy>
</Xtensive.Orm.Reprocessing>
</Orm.Extensions>
</configuration>
or in JSON
{
"Orm.Extensions": {
"Xtensive.Orm.Reprocessing": {
"DefaultTransactionOpenMode" : "New",
"DefaultExecuteStrategy" : "Xtensive.Orm.Reprocessing.HandleReprocessableExceptionStrategy, Xtensive.Orm.Reprocessing"
}
}
}
Then section must be provided manually, code may look like
var app = builder.Build();
//...
var configurationRoot = app.Configuration;
var extensionsGroupSection = configurationRoot.GetSection("Orm.Extensions");
var reprocessingSection = extensionsGroupSection.GetSection("Xtensive.Orm.Reprocessing");
var reprocessingConfig = ReprocessingConfiguration.Load(reprocessingSection);
domainConfiguration.ExtensionConfigurations.Set(reprocessingConfig);
or with use of extension method
var app = builder.Build();
//...
var configurationRoot = app.Configuration;
var extensionsGroupSection = configurationRoot.GetSection("Orm.Extensions");
var reprocessingSection = extensionsGroupSection.GetSection("Xtensive.Orm.Reprocessing");
domainConfiguration.ConfigureReprocessingExtension(reprocessingSection);
The extension provides security layer (authentication services, principals, roles, secured queries) There are 2 main parts that can also be used separately: authentication services and role-based access to domain entities
<Xtensive.Orm>
<domains>
<domain ... >
<types>
<add assembly="your assembly"/>
<add assembly="Xtensive.Orm.Security"/>
</types>
</domain>
</domains>
</Xtensive.Orm>
<section name="Xtensive.Orm.Security" type="Xtensive.Orm.Security.Configuration.ConfigurationSection,
Xtensive.Orm.Security" />
and set up the desired hashing service:
<Xtensive.Orm.Security>
<hashingService name="plain"/>
<!-- other options are: md5, sha1, sha256, sha384, sha512 -->
</Xtensive.Orm.Security>
[HierarchyRoot]
public class User : GenericPrincipal
{
[Field, Key]
public int Id { get; private set; }
[Field]
public string LastName { get; set; }
[Field]
public string FirstName { get; set; }
...
public User(Session session) : base(session) {}
}
// Creating a user
using (var session = Domain.OpenSession()) {
using (var transaction = session.OpenTransaction()) {
var user = new User(session);
user.Name = "admin";
user.SetPassword("password");
transaction.Complete();
}
}
// Authenticating a user
using (var session = Domain.OpenSession()) {
using (var transaction = session.OpenTransaction()) {
var user = session.Authenticate("admin", "password");
transaction.Complete();
}
}
EmployeeRole
|
|- StockManagerRole
|
|- SalesRepresentativeRole
|
|- SalesManagerRole
|
|- SalesPresidentRole
// This is base role for all employees
[HierarchyRoot(InheritanceSchema = InheritanceSchema.SingleTable)]
public abstract class EmployeeRole : Role
{
[Field, Key]
public int Id { get; set; }
protected override void RegisterPermissions()
{
// All employees can read products
RegisterPermission(new Permission<Product>());
// All employees can read other employees
RegisterPermission(new Permission<Employee>());
}
protected EmployeeRole(Session session)
: base(session) {}
}
public class StockManagerRole : EmployeeRole
{
protected override void RegisterPermissions()
{
// Stock manager inherits Employee permissions
base.RegisterPermissions();
// Stock manager can read and write products
RegisterPermission(new Permission<Product>(canWrite:true));
}
public StockManagerRole(Session session)
: base(session) {}
}
// Create instances of roles on first domain initialization
using (var session = Domain.OpenSession()) {
using (var transaction = session.OpenTransaction()) {
new SalesRepresentativeRole(session);
new SalesManagerRole(session);
new SalesPresidentRole(session);
new StockManagerRole(session);
transaction.Complete();
}
}
using (var session = Domain.OpenSession()) {
using (var transaction = session.OpenTransaction()) {
var stockManagerRole = session.Query.All<StockManagerRole>().Single();
var user = new User(session);
user.Name = "peter";
user.SetPassword("password");
user.Roles.Add(stockManagerRole);
transaction.Complete();
}
}
user.IsInRole("StockManagerRole");
// or
user.Roles.Contains(stockManagerRole);
using (var imContext = session.Impersonate(user)) {
// inside the region the session is impersonated with the specified
// principal and set of their roles and permissions
// Checking whether the user has a permission for reading Customer entities
imContext.Permissions.Contains<Permission<Customer>>(p => p.CanRead);
// Checking whether the user has a permission for writing to Customer entities
imContext.Permissions.Contains<Permission<Customer>>(p => p.CanWrite);
// another way
var p = imContext.Permissions.Get<Permission<Customer>>();
if (p != null && p.CanRead)
// allow doing some stuff
}
To end the impersonation call ImpersonationContext.Undo() or Dispose() method.
Impersonation contexts can be nested, e.g.:
using (var userContext = session.Impersonate(user)) {
// do some user-related stuff
using (var adminContext = session.Impersonate(admin)) {
// do some admin stuff
}
// we are still in user impersonation context
}
// no context here
7. Secure (restrictive) queries
A role may set up a condition that will be automatically added to any query and filters the query results, e.g.:
public class AutomobileManagerRole : EmployeeRole
{
private static IQueryable<Customer> GetCustomers(ImpersonationContext context, QueryEndpoint query)
{
return query.All<Customer>()
.Where(customer => customer.IsAutomobileIndustry);
}
protected override void RegisterPermissions()
{
base.RegisterPermissions();
// This permission tells that a principal can read/write customers
// but only those that are returned by the specified condition
RegisterPermission(new CustomerPermission(true, GetCustomers));
}
public AutomobileManagerRole(Session session)
: base(session) {}
}
Now all employees that have AutomobileManagerRole will read customers that have IsAutomobileIndustry property set to true, e.g.:
using (var session = Domain.OpenSession()) {
using (var transaction = session.OpenTransaction()) {
var automobileManagerRole = session.Query.All<AutomobileManagerRole>().Single();
var user = new User(session);
user.Name = "peter";
user.SetPassword("password");
user.Roles.Add(automobileManagerRole);
using (var context = session.Impersonate(user)) {
var customers = Query.All<Customer>();
// Inside the impersonation context the above-mentioned query condition
// will be added automatically so user will get only automobile customers
}
transaction.Complete();
}
}
Additionally to “How to use” section it provides extra examples of how to configure and/or read extension configuration.
The example in “How to use” section uses old fasioned API of configuration files, yet usable in many applications. But there are some cases which may require usage of different API or work-around certain cases with existing one.
Example #1 Reading old-style configuration of an assembly in NET 5 and newer.
Due to new architecture without AppDomain (which among the other things was in charge of gathering configuration files of loaded assemblies as it would be one configuration file) System.Configuration.ConfigurationManager now reads only configuration file of actual executable, loaded assemblies’ configuration files stay unreachable by default, though there is need to read some data from them. A great example is test projects which are usually get loaded by test runner executable, and the only configuration accessible in this case is test runner one.
Extra step is required to read configuration files in such cases. Thankfully, ConfigurationManager has methods to get access to assemblies’ configurations.
To get access to an assembly configuration file it should be opened explicitly by
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
The instance returned from OpenExeConfiguration provides access to sections of the assembly configuration. DataObjects.Net configurations (DomainConfiguration, SecurityConfiguration, etc.) have Load() methods that can recieve this instance. SecurityConfiguration can be read like so
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
var securityConfig = SecurityConfiguration.Load(configuration);
// loaded configuration should be manually placed to
domainConfiguration.ExtensionConfigurations.Set(securityConfig);
The domainConfiguration.ExtensionConfigurations is a new unified place from which the extension will try to get its configuration instead of calling default parameterless Load() method, which has not a lot of sense now, though the method is kept as a second source for backwards compatibility.
For more convenience, DomainConfiguration extensions are provided, which make code more neater. For instance,
var configuration = ConfigurationManager.OpenExeConfiguration(typeof(SomeTypeInConfigOwnerAssembly).Assembly.Location);
var domainConfiguration = DomainConfiguration.Load(configuration);
// the extension hides getting configuration with SecurityConfiguration.Load(configuration)
// and also putting it to ExtensionConfigurations collection.
domainConfiguration.ConfigureSecurityExtension(configuration);
Remember the requirement to register Xtensive.Orm.Security to domain? The extension tries to register this assembly to DomainConfiguration.Types collection so even if you miss registration but called extension method required types of Security extension will be registered in Domain types.
Custom section names are also supported if for some reason default section name is not used.
Example #2 Reading old-style configuration of an assembly in a project that uses appsettings.json file.
If for some reason there is need to keep the old-style configuration then there is a work-around as well. Static configuration manager provides method OpenMappedExeConfiguration() which allows to get access to any *.config file as System.Configuration.Configuration instance. For example,
ExeConfigurationFileMap configFileMap = new ExeConfigurationFileMap();
configFileMap.ExeConfigFilename = "Orm.config"; //or other file name, the file should exist bin folder
var configuration = System.Configuration.ConfigurationManager.OpenMappedExeConfiguration(configFileMap, ConfigurationUserLevel.None);
After that, as in previous example, the instance can be passed to Load method of SecurityConfiguration to read extension configuration and later put it to DomainConfiguration.ExtensionConfigurations
var securityConfiguration = SecurityConfiguration.Load(configuration);
domainConfiguration.ExtensionConfigurations.Set(securityConfiguration);
Extension usage will look like
domainConfiguration.ConfigureSecurityExtension(configuration);
Example #3 Configure using Microsoft.Extensions.Configuration API.
This API allows to have configurations in various forms including JSON and XML formats. Loading of such files may differ depending on .NET version, check Microsoft manuals for instructions.
Allowed JSON and XML configuration definition look like below
<configuration>
<Xtensive.Orm.Security>
<HashingService>sha512</HashingService>
<AuthenticationService>CustomAuthenticationService</AuthenticationService>
</Xtensive.Orm.Security>
</configuration>
{
"Xtensive.Orm.Security": {
"HashingService" : "sha512",
"AuthenticationService" : "CustomAuthenticationService"
}
}
The API has certain issues with XML elements with attributes so it is recommended to use more up-to-date attributeless nodes. For JSON it is pretty clear, almost averyone knows its format.
SecurityConfiguration.Load method can accept different types of abstractions from the API, including - Microsoft.Extensions.Configuration.IConfiguration; - Microsoft.Extensions.Configuration.IConfigurationRoot; - Microsoft.Extensions.Configuration.IConfigurationSection.
Loading of configuration may look like
var app = builder.Build();
//...
// tries to load from default section "Xtensive.Orm.Security"
var securityConfig = SecurityConfiguration.Load(app.Configuration);
domainConfiguration.ExtensionConfigurations.Set(securityConfig);
or, with use of extension, like
var app = builder.Build();
//...
// Tries to load from default section "Xtensive.Orm.Security"
// and put it into domainConfiguration.ExtensionConfigurations.
// Additionally, registers types of "Xtensive.Orm.Security" assembly.
domainConfiguration.ConfigureSecurityExtension(app.Configuration);
Example #4 Configure using Microsoft.Extensions.Configuration API from section with non-default name.
For configurations like
<configuration>
<Orm.Security>
<HashingService>sha512</HashingService>
<AuthenticationService>CustomAuthenticationService</AuthenticationService>
</Orm.Security>
</configuration>
{
"Orm.Security": {
"HashingService" : "sha512",
"AuthenticationService" : "CustomAuthenticationService"
}
}
Loading of configuration may look like
var app = builder.Build();
//...
var securityConfig = SecurityConfiguration.Load(app.Configuration, "Orm.Security");
domainConfiguration.ExtensionConfigurations.Set(securityConfig);
or, with use of extension, like
var app = builder.Build();
//...
domainConfiguration.ConfigureSecurityExtension(app.Configuration, "Orm.Security");
Example #5 Configure using Microsoft.Extensions.Configuration API from sub-section deeper in section tree.
If for some reason extension configuration should be moved deeper in section tree like something below
<configuration>
<Orm.Extensions>
<Xtensive.Orm.Security>
<HashingService>sha512</HashingService>
<AuthenticationService>CustomAuthenticationService</AuthenticationService>
</Xtensive.Orm.Security>
</Orm.Extensions>
</configuration>
or in JSON
{
"Orm.Extensions": {
"Xtensive.Orm.Security": {
"HashingService" : "sha512",
"AuthenticationService" : "CustomAuthenticationService"
}
}
}
Then section must be provided manually, code may look like
var app = builder.Build();
//...
var configurationRoot = app.Configuration;
var extensionsGroupSection = configurationRoot.GetSection("Orm.Extensions");
var securitySection = extensionsGroupSection.GetSection("Xtensive.Orm.Security");
var securityConfig = SecurityConfiguration.Load(securitySection);
domainConfiguration.ExtensionConfigurations.Set(securityConfig);
or, with use of extension method, like
var app = builder.Build();
//...
var configurationRoot = app.Configuration;
var extensionsGroupSection = configurationRoot.GetSection("Orm.Extensions");
var securitySection = extensionsGroupSection.GetSection("Xtensive.Orm.Security");
domainConfiguration.ConfigureSecurityExtension(securitySection);
The extension provides tracking/auditing funtionality on Session/Domain level.
How to use
<Xtensive.Orm>
<domains>
<domain ... >
<types>
<add assembly="your assembly"/>
<add assembly="Xtensive.Orm.Tracking"/>
</types>
</domain>
</domains>
</Xtensive.Orm>
4. Subscribe to TrackingCompleted event. After each tracked transaction is committed you receive the TrackingCompletedEventArgs object.
5. TrackingCompletedEventArgs.Changes contains a collection of ITrackingItem objects, each of them represents a set of changes that occurred to an Entity within the transaction committed.
var monitor = Domain.Services.Get<IDomainTrackingMonitor>();
monitor.TrackingCompleted += TrackingCompletedListener;
using (var session = Domain.OpenSession()) {
using (var t = session.OpenTransaction()) {
var e = new MyEntity(session);
e.Text = "some text";
t.Complete();
}
}
private void TrackingCompletedListener(object sender, TrackingCompletedEventArgs e)
{
foreach (var change in e.Changes) {
Console.WriteLine(change.Key);
Console.WriteLine(change.State);
foreach (var value in change.ChangedValues) {
Console.WriteLine(value.Field.Name);
Console.WriteLine(value.OriginalValue);
Console.WriteLine(value.NewValue);
}
}
}
The extension adds integration for DataObjects.Net and ASP.NET Core. NOTE, that SessionManager class is deprecated. We strongly recommend not to use SessionManager because it, it does not keep up with current trends.
New types use more natural mechanisms of ASP.NET Core such as Pipeline, dependancy injection (DI) and Action filters. It instead of static access to session by using Session.Current, it is designed to provide a session instance directly.
The extension contains two ways of integration - by using Action Filters or by using Middleware. Both are available for ASP.Net MVC applications, Rasor Pages have to use only middleware.
The extension contains SessionActionFilter class. This filter opens Session and TransacionScope before an action executed, puts them to request’s HttpContext and provides you a SessionAccessor instance which is capable of getting the objects from the context. When action is completed, the action filter removes Session and TransactionScope instances from the context and disposes them.
SessionActionFilter can be registered as action filter in Startup class like so:
public void ConfigureServices(IServiceCollection services)
{
var domain = Domain.Build();
services.AddSingleton<Domain>(domain);// make domain accessible as service in action filter
services.AddDataObjectsSessionAccessor();// adds SessionAccessor as scoped service
services.AddControllersWithViews(options => options.Filters.AddDataObjectsSessionActionFilter());
}
After that you use it like so:
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
// This action require Session and TransactionScope because it
// has SessionAccessor as parameter. So the action filter will open them.
public IActionResult Index([FromServices] SessionAccessor sessionAccessor)
{
// DO NOT dispose them within action because the action filter
// controlls them.
var session = sessionAccessor.Session;
var transactionScope = sessionAccessor.TransactionScope;
// some query to database
var someQuery = session.Query.All<SomeEntity>().ToList()
...
return View();
}
// This action does not require Session and TransactionScope
// to be opened. In this case filter won't do any work - neither Session
// nor TransactionScope will be opened.
public IActionResult Privacy()
{
return View();
}
public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}
}
Notice [FromServices] attribute for parameter, it is required.
The filter has an ability to skip openings if action does not require them. So the action filter gives you access to database when only you declare you need it.
By default, the filter completes the opened transaction scope unless an exception has appeared. It was done so to mimic the work of obsolete SessionManager which also completes transaction scope by default. If you want to change default behavior you can create your own filter based on ours and override CompleteTransactionOnException method and define what exceptions will not rollback transaction.
All the methods you can override:
The methods are called in the same order.
The extension also has OpenSessionMiddleware class. The key feature of action filter is also its drawback - it wraps only MVC action. If wider coverage needed then it’s better to use middleware because it can wrap not only MVC action calls but also controller’s constructor calls and other middleware which are defined down the pipeline.
To confure your app to use OpenSessionMiddleware, first, configure services in Startup class like so:
public void ConfigureServices(IServiceCollection services)
{
var domain = Domain.Build();
services.AddSingleton<Domain>(domain); // to have domain be accessible
//infrastructure will create SessionAccessor instance
//for each request.
services.AddDataObjectsSessionAccessor();
services.AddControllersWithViews();
}
Then, put OpenSessionMiddleware to desired place in the pipeline like so:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseStaticFiles()
.UseAuthorization()
.UseDataObjectsSessionOpener()
// ...
// any other middlewhare,
// they will have access to
// opened session
// ...
.UseRouting()
.UseEndpoints(endpoints => {
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
After that you can specify SessionAccessor as a parameter in MVC controllers’ constructors, actions. The SessionOpener will open Session and TransactionScope on going forward and dispose them on going backwards the pipeline.
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;
public IActionResult Index([FromServices] SessionAccessor sessionAccessor)
{
var session = sessionAccessor.Session;
var transactionScope = sessionAccessor.TransactionScope;
// some query to database
var someQuery = session.Query.All<SomeEntity>().ToList()
...
return View();
}
public IActionResult Privacy()
{
return View();
}
public HomeController(ILogger<HomeController> logger, SessionAccessor accessor)
{
_logger = logger;
}
}
Other middleware can get access to session like so:
public class SomeCustomMiddleware
{
private readonly RequestDelegate next;
// Dependancy Injection mechansims will provide accessor
public async Task Invoke(HttpContext httpContext, SessionAccessor sessionAccessor)
{
// perform some actions before next middleware
await next.Invoke(httpContext);
// some actions after execution returned
}
public SomeCustomMiddleware(RequestDelegate next)
{
this.next = next
}
}
As the action filter above it completes transaction scope by default unless an exception happens. It also re-throw the exception by default.
OpenSessionMiddleware is customizable as well. It has similar CompleteTransactionOnException method to control what exceptions will not rollback transaction. Additionally, RethrowException method allows to define what exceptions should be “swallowed”.
Besides these two methods the class contains following virtual methods:
OpenSessionMiddleware and SessionActionFilter will work together just fine. If the action filter detects that Session and TransactionScope have already been opended by the middleware it will not open another one and let the middleware be in charge.