Upgrading to Serenity.NET 5
This document outlines steps required to upgrade an existing Serenity .NET Core 3.1 based project (3.14.x) to Serenity.NET 5.0 (5.0.12)
If you have to not migrated from ASP.NET MVC to ASP.NET Core yet, please follow this document first:
Using Stargen
StartSharp customers may follow instructions in this document to use Stargen:
Editing Project File
Edit YourProject.csproj
and change target framework:
From:
<TargetFramework>netcoreapp3.1</TargetFramework>`
To:
<TargetFramework>net5.0</TargetFramework>
If you are using GIT it is recommended to stage your changes on every step to be able to undo your changes if you make a mistake during following steps as we'll do many bulk operations.
Changes in Usage of Serenity as Submodule Case
If you use serenity as submodule instead nuget packages, you need update package reference, their condition paths paths and project reference paths.
From:
<PackageReference Include="Serenity.Scripts" Version="3.14.5" />
<PackageReference Include="Serenity.Web" Version="3.14.5" Condition="!Exists('..\..\Serenity\Serenity.Core\Serenity.Core.csproj')" />
<PackageReference Include="Serenity.Web.Assets" Version="3.14.4" />
To:
<PackageReference Include="Serenity.Scripts" Version="5.0.0" />
<PackageReference Include="Serenity.Web" Version="5.0.6" Condition="!Exists('..\..\Serenity\src\Serenity.Net.Core\Serenity.Net.Core.csproj')" />
<PackageReference Include="Serenity.Web.Assets" Version="3.14.4" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.1" />
From:
<ItemGroup Condition="Exists('..\..\Serenity\Serenity.Core\Serenity.Core.csproj')">
<ProjectReference Include="..\..\Serenity\Serenity.Core\Serenity.Core.csproj" />
<ProjectReference Include="..\..\Serenity\Serenity.Data.Entity\Serenity.Data.Entity.csproj" />
<ProjectReference Include="..\..\Serenity\Serenity.Data\Serenity.Data.csproj" />
<ProjectReference Include="..\..\Serenity\Serenity.Services\Serenity.Services.csproj" />
<ProjectReference Include="..\..\Serenity\Serenity.Web\Serenity.Web.csproj" />
To:
<ItemGroup Condition="Exists('..\..\Serenity\src\Serenity.Net.Core\Serenity.Net.Core.csproj')">
<ProjectReference Include="..\..\Serenity\src\Serenity.Net.Core\Serenity.Net.Core.csproj" />
<ProjectReference Include="..\..\Serenity\src\Serenity.Net.Entity\Serenity.Net.Entity.csproj" />
<ProjectReference Include="..\..\Serenity\src\Serenity.Net.Data\Serenity.Net.Data.csproj" />
<ProjectReference Include="..\..\Serenity\src\Serenity.Net.Services\Serenity.Net.Services.csproj" />
<ProjectReference Include="..\..\Serenity\src\Serenity.Net.Web\Serenity.Net.Web.csproj" />
From:
<Import Project="$(SolutionDir)Serenity\tools\Submodule\Serenity.Submodule.AspNetCore.targets" Condition="Exists('$(SolutionDir)Serenity\tools\Submodule\Serenity.Submodule.AspNetCore.targets')" />
To:
<Import Project="$(SolutionDir)Serenity\build\submodule.targets" Condition="Exists('$(SolutionDir)Serenity\build\submodule.targets')" />
Changing References to Row with IRow interface
Serenity base Row class is replaced with a generic row class which implements new IRow interface so any references in your project to old Row class should be replaced with IRow.
It might be hard to replace all such references manually, so we'll use a trick to make Visual Studio do it for us automatically.
Create an empty .CS file in your project like RowTrick.cs
:
namespace Serenity.Data
{
public class Row
{
}
}
Now put your editing cursor. onto Row
word and press Ctrl+R+R or Ctrl+F2 (based on your editor shortcuts) or right click Row
and click Rename...
and type IRow
then press Enter
to apply changes.
Now you may delete RowTrick.cs
file.
Changing base class of rows to Row<TRowFields>
As noted in previous section, base Row
class is replaced with a generic row class Row<TRowFields>
which provides a direct reference to a rows fields type.
So if you used to had:
public class MyRow : Row
{
public class RowFields
{
}
}
You will need to replace it with:
public class MyRow : Row<MyRow.RowFields>
{
public class RowFields
{
}
}
Again this might take too much time for a manual process, so let's use a Regex replace:
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
class\s*([A-Za-z]*)Row\s*:\s*I?Row(\s*[^<]{1})
inFind
inputType
class $1Row : Row<$1Row.RowFields>$2
inReplace
inputClick
Replace All
Save all open files if any
Removing Static RowFields Instance from Rows
We used to have a static RowFields instance named Fields
in row classes which is no longer required as the base generic row class already have a correctly typed static Fields
property with the same name.
This new design will open way to having different sets of fields for multi tenant row customization scenarios in the future.
Open
Replace in Fields
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
[\r]?[\n]?[ \t]*public\s+static\s+readonly\s+RowFields\s+Fields\s*=\s*new\s+RowFields\(\)\.Init\(\);\r?\n
inFind
inputClear
Replace
input value (empty string)Click
Replace All
Adding Row Constructors to Accept a RowFields Instance
Row constructors used to pass the static RowFields instance to base Row constructor like this:
public CustomerRow()
: base(Fields)
{
}
As there is no longer a static Fields instance, we'll keep the default constructor removing the base call and also add a new constructor that accepts a RowFields instance.
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([ \t]*public[ \t]* )([A-Za-z]*)Row\(\)([\s\r\n]*:[\s]*base\()Fields\)(\s*\{\s*\})([\r]?[\n]?)
inFind
inputType
$1$2Row()$4$5$5$1$2Row(RowFields fields)$3fields)$4$5
inReplace
inputClick
Replace All
Save all open files if any
Replacing IIdRow.IdField with [IdProperty] Attribute
In your rows you might you have IIdRow
interface implemented like below:
// we will delete this
IIdField IIdRow.IdField
{
get { return Fields.CustomerId; }
}
This interface is still there, but it does not have IdField
property now.
We replaced it with [IdProperty]
attribute as old explicit style made determining that field using reflection (without creating a row instance first) difficult.
You need to find all such rows by searching for IIdRow.IdField
. Then take a note of IdField
, delete the block and put a [IdProperty]
attribute on corresponding property if required (read following paragraph).
Please note that if you only have one property with [Identity] attribute in your row class and it matches the ID property you don't need to put [IdProperty] explicitly. Same applies to [PrimaryKey] as long as it is just one.
Tip: Fields with
[Identity]
attribute implicitly has thePrimaryKey
flag as it is a combination of AutoIncrement and PrimaryKey. If you have an auto increment field that is not a primary key, it should be marked with[AutoIncrement]
not[Identity]
.
The order for precedence to determine the ID property is [IdProperty] => Single [Identity] => Single [PrimaryKey].
So if your ID property can't be determined by [Identity] or [PrimaryKey] attributes, or if you prefer to be explicit, add [IdProperty] like this:
[..., IdProperty]
public Guid? CustomerId
{
get { return Fields.CustomerId[this]; }
set { Fields.CustomerId[this] = value; }
}
If you prefer to do it with search / replace again, here is the way (warning might be slow due to complexity of the regex):
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
(\[[^\{\}]*)(\][\r\n\s]*public [A-Za-z0-9]+\?? )([A-Za-z]+)(\s*\r?\n+[\s\S\n]+)(\r?\n\s*IIdField\s*IIdRow.IdField[\s\r\n]*(\{[\r\n\s]*get[r\n\s]*\{[\r\n\s]*return|=>\s*) Fields.)(\3)([\s]*;([\r\n\s]*\}[\r\n\s]*\}|))\r?\n?\r?\n?
inFind
inputType
$1, IdProperty$2$3$4
inReplace
inputClick
Replace All
Save all open files if any
Replacing INameRow.NameField with [NameProperty] Attribute
Just like IIdRow, you may also you have INameRow
interface implemented like below:
// we will delete this
StringField INameRow.NameField
{
get { return Fields.CustomerName; }
}
Again, this interface is still there, but it does not have NameField
property now.
We replaced it with [NameProperty]
attribute as old explicit style made determining that field without creating a row instance first difficult, and it had to be a string field before.
You need to find all such rows by searching for INameRow.NameField
. Then take a note of NameField
, delete the block, and put a [NameProperty]
attribute on corresponding property.
For the example above, CustomerName property will be like this:
[..., NameProperty]
public String CustomerName
{
get { return Fields.CustomerName[this]; }
set { Fields.CustomerName[this] = value; }
}
If you prefer to do it with search / replace again, here is the way (warning might be slow due to complexity of the regex):
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
(\[[^\{\}]*)(\][\r\n\s]*public [A-Za-z0-9]+\?? )([A-Za-z]+)(\s*\r?\n+[\s\S\n]+)(\r?\n\s*StringField\s*INameRow.NameField[\s\r\n]*(\{[\r\n\s]*get[r\n\s]*\{[\r\n\s]*return|=>\s*) Fields.)(\3)([\s]*;([\r\n\s]*\}[\r\n\s]*\}|))\r?\n?
inFind
inputType
$1, NameProperty$2$3$4
inReplace
inputClick
Replace All
Save all open files if any
Replacing IIdField
References with Field
The IIdField interface is removed, and interfaces like IInsertLogRow and IUpdateLogRow uses Field
instead of IIdField
.
So if you had these (for example in LoggingRow.cs):
IIdField IInsertLogRow.InsertUserIdField
{
get { return loggingFields.InsertUserId; }
}
IIdField IUpdateLogRow.UpdateUserIdField
{
get { return loggingFields.UpdateUserId; }
}
Need to replace them with these:
Field IInsertLogRow.InsertUserIdField
{
get { return loggingFields.InsertUserId; }
}
Field IUpdateLogRow.UpdateUserIdField
{
get { return loggingFields.UpdateUserId; }
}
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
(\s?)IIdField(\s+)
inFind
inputType
$1Field$2
inReplace
inputClick
Replace All
Save all open files if any
Using fields
instead of Fields
in Row Properties
The new static Fields
instance in base Row class is resolved from a RowFieldsProvider
for current thread context everytime it is accessed.
We used to have such properties in Row.cs:
public int? CountryID
{
get { return Fields.CountryID[this]; }
set { Fields.CountryID[this] = value; }
}
We need to replace Fields
with fields
as they might be different in rare cases (multi-tenant scenarios). Also it is much slower to access Fields
than fields
.
We'll replace them like below (also using new property syntax):
public int? CountryID
{
get => fields.CountryID[this];
set => fields.CountryID[this] = value;
}
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
(^[\t ]*)(get|set)[ \t]*(\=\>|\{)[\t ]*(return|)[\t ]*F(ields\.[^\^;}]*)\;[\t ]*\}?[\t ]*\r?$
inFind
inputType
$1$2 => f$5;
inReplace
inputClick
Replace All
Save all open files if any
Replacing Check.NotNull Calls
We used to have a helper class to check for nulls and empty strings etc. but Visual Studio analyzer still raised warnings even if you check for null using those helpers.
So, we decided to remove that class completely.
If you had a check like below:
Check.NotNull(request.Something, nameof(request.Something));
Replace it with:
if (request.Something == null)
throw new ArgumentNullException(nameof(request.Something));
Here is the search replace method that can do it:
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)Check.NotNull\(([A-Za-z0-9\.]+),\s*([^\n\r]*)\);([\r]?\n)
inFind
inputType
in$1if ($2 is null)$4$1 throw new ArgumentNullException($3);$4
Replace
inputClick
Replace All
Replacing Check.NotNullOrEmpty Calls
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)Check.NotNullOrEmpty\(([A-Za-z0-9\.]+),\s*([^\n\r]*)\);([\r]?\n)
inFind
inputType
in$1if (string.IsNullOrEmpty($2))$4$1 throw new ArgumentNullException($3);$4
Replace
inputClick
Replace All
Replacing Check.NotNullOrWhiteSpace Calls
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)Check.NotNullOrWhiteSpace\(([A-Za-z0-9\.]+),\s*([^\n\r]*)\);([\r]?\n)
inFind
inputType
in$1if (string.IsNullOrWhiteSpace($2))$4$1 throw new ArgumentNullException($3);$4
Replace
inputClick
Replace All
Save all open files if any
Replacing request.CheckNotNull Calls
If you had a check like below:
request.CheckNotNull();
Replace it with:
if (request is null)
throw new ArgumentNullException(nameof(request));
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)request.CheckNotNull\(\);([\r]?\n)
inFind
inputType
in$1if (request is null)$2$1 throw new ArgumentNullException(nameof(request));$2
Replace
inputClick
Replace All
Save all open files if any
Add System Namespace to Files with Errors
You might get The type or namespace name 'ArgumentNullException'...
after doing prior changes, so please add using System;
to any file that you get that error in, for example SergenEndpoint.cs
and UserPreferenceRepository.cs
Moving BackgroundJobManager.Settings Class Outside
If you have a project created from a recent StartSharp template, you may have a BackgroundJobManager.cs
file like below:
//...
isDisabled = !Config.Get<Settings>().Enabled;
//...
public static void Register(IBackgroundJob task)
{
Process();
}
[SettingScope("Application"), SettingKey("BackgroundJobs")]
public class Settings
{
public bool Enabled { get; set; }
}
}
}
The Settings class is a nested one, and we need to move it outside the BackgroundJobManager class, and rename it to BackgroundJobSettings
:
//...
isDisabled = !Config.Get<BackgroundJobSettings>().Enabled;
//...
public static void Register(IBackgroundJob task)
{
Process();
}
}
[SettingScope("Application"), SettingKey("BackgroundJobs")]
public class BackgroundJobSettings
{
public bool Enabled { get; set; }
}
}
Replacing SettingScope
and SettingKey
Serenity applications used to have settings like following which are read through a special Config
class when required:
[SettingScope("Application"), SettingKey("MyCustomKey")]
public class MyCustomSettings
{
}
We removed the Config
class and will be using .NET Core configuration / options instead.
See following documents on how it works: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-5.0 https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-5.0
Previous class will be replaced with following:
[SettingScope("Application"), SettingKey("MyCustomKey")]
public class MyCustomSettings
{
public const string ConfigurationPath = "AppSettings:MyCustomKey";
}
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
[\t ]*\[[\t ]*(SettingScope\(.*\)\,|)[\t ]*SettingKey\("(.*)"\)\][\t ]*\r?\n[\s\r\n]*public class[\t ]*([A-Za-z0-9]*)[\t ]*(\r?\n)[\r\n]*([\t ]*)\{
inFind
inputType
$5// services.Configure<$3>(Configuration.GetSection($3.SectionKey));$4$5public class $3$4$5{$4$5 public const string SectionKey = "$2";$4
inReplace
inputClick
Replace All
Now your setting class will turn into this:
// services.Configure<InstalledModuleSettings>(Configuration.GetSection(MyCustomSettings.SectionKey));
public class MyCustomSettings
{
public const string SectionKey = "MyCustomKey";
}
Please copy the commented like (except //) and paste it inside Startup.cs
-> ConfigureServices
method:
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<MyCustomSettings>(Configuration.GetSection(MyCustomSettings.SectionKey));
//...
}
You may need to add the namespace for MyCustomSettings to usings in Startup.cs.
After that delete the commented line above MyCustomSettings.
And finally open appsettings.json
file and move the setting inside AppSettings
subsection to root:
{
"AppSettings": {
// ...before lines
"MyCustomKey": {
}
// ...after lines
}
}
should be replaced with:
{
"AppSettings": {
// ...before lines
// ...after lines
},
"MyCustomKey": {
}
}
Serenity used to keep its own settings under
AppSettings:
for historical reasons. This prefix is no longer used.
Repeat steps above for all settings classes you have.
Migrating EnvironmentSettings
As EnvironmentSettings class under Modules/Membership/Account/EnvironmentSettings.cs
does not have SettingKey
attribute, modify the class like below and repeat changes in appsettings.json
and Startup.cs
for it.
public class EnvironmentSettings
{
public const string SectionKey = "EnvironmentSettings";
public string SiteExternalUrl { get; set; }
}
Configuring Serenity Options
Just like we configured your custom settings with .NET options system in the last section, we also need to configure Serenity options.
Open Startup.cs
file and add following under ConfigureServices
method:
using Serenity.Data;
using Serenity.Web;
public void ConfigureServices(IServiceCollection services)
{
//...
services.Configure<ConnectionStringOptions>(Configuration.GetSection(ConnectionStringOptions.SectionKey));
services.Configure<CssBundlingOptions>(Configuration.GetSection(CssBundlingOptions.SectionKey));
services.Configure<LocalTextPackages>(Configuration.GetSection(LocalTextPackages.SectionKey));
services.Configure<ScriptBundlingOptions>(Configuration.GetSection(ScriptBundlingOptions.SectionKey));
services.Configure<UploadSettings>(Configuration.GetSection(UploadSettings.SectionKey));
//...
}
Edit appsettings.json
and move following out from AppSettings
:
{
"AppSettings": {
// ...before lines
"CssBundling": {
},
"ScriptBundling": {
},
"UploadSettings": {
},
"LocalTextPackages": {
}
// ...after lines
}
}
should be replaced with:
{
"AppSettings": {
// ...before lines
// ...after lines
},
"CssBundling": {
},
"ScriptBundling": {
},
"UploadSettings": {
},
"LocalTextPackages": {
}
}
You may delete AppSettings
key if nothing left under it after doing this.
Migrating LocalTextPackages Setting
LocalTextPackages determines the list of text bundles that are allowed to be sent to the browser. They used to be a list of prefixes:
"LocalTextPackages": {
"Site": [
"Controls.",
"Db.",
"Dialogs.",
"Enums.",
"Forms.",
"Permission.",
"Site.",
"Validation."
],
"Login": [
"Forms.Membership.Login.",
"Db.Administration.User.",
"Validation.Required",
"Dialogs."
]
}
In the new version, packages are defined as a simple regular expression instead of an array of prefixes, so the previous setting in appsettings.json
should be replaced with following:
"LocalTextPackages": {
"Site": "^(Controls|Db|Dialogs|Enums|Forms|Permission|Site|Validation)\\.",
"Login": "^(Forms\\.Membership\\.Login|Db\\.Administration\\.User|Validation\\.Required|Dialogs)\\."
}
Simply put a
^(
followed by previous prefixes separated by|
then)\\.
, while.
inside the prefixes should be replaced with\\.
. If you haven't modified anything there, then just use our sample.
Migrating CssBundles.json and ScriptBundles.json to Options System
We used to have wwwroot/Scripts/site/ScriptBundles.json
and wwwroot/Content/site/CssBundles.json
files. These bundles are still there but needs to be migrated to .NET options. Their contents should be moved to relevant sections inside a appsettings.bundles.json
file.
So if you have CssBundles.json
file like this:
{
"Libs": [
"~/Content/font-open-sans.css",
// ...
],
"Site": [
"~/Content/site/site.css"
// ...
]
}
and a ScriptBundles.json
file like this:
{
"Libs": [
"~/Scripts/pace.js",
// ...
],
"Site": [
"~/Scripts/adminlte/app.js",
// ...
]
}
Create a appsettings.bundles.json
file next to appsettings.json
file and copy existing content under ScriptBundles.json
and CssBundles.json
like this:
{
"CssBundling": {
"Bundles": { // this part is from CssBundles.json
"Libs": [
"~/Content/font-open-sans.css",
// ...
],
"Site": [
"~/Content/site/site.css"
// ...
]
}
},
"ScriptBundling": {
"Bundles": { // this part is from ScriptBundles.json
"Libs": [
"~/Scripts/pace.js",
// ...
],
"Site": [
"~/Scripts/adminlte/app.js",
// ...
]
}
}
}
Next we need to set this file as a JSON config source. Open Program.cs
and add following line before appsettings.machine.json
:
config.AddJsonFile("appsettings.bundles.json");
config.AddJsonFile("appsettings.machine.json", optional: true);
You may delete CssBundles.json
and ScriptBundles.json
afterwards.
Replacing Config.Get<TSettings>
Calls
As noted in previous sections, Config
class is removed and we'll be using .NET options pattern instead.
Unfortunately this is not something we can do simply with Search/Replace.
We'll list some samples below for your custom cases, and will show steps for DataExplorer, BackgroundJobManager in the following sections
If you were reading configuration in a Controller:
public class MyController : Controller
{
public ActionResult SomeAction(...)
{
var config = Config.Get<MyCustomSettings>();
}
}
You may replace it like this:
using Microsoft.Extensions.Options;
public class MyController : Controller
{
public ActionResult SomeAction([FromServices] IOptions<MyCustomSettings> myCustomSettings...)
{
var config = myCustomSettings.Value;
}
}
Or if you prefer constructor injection:
using Microsoft.Extensions.Options;
public class MyController : Controller
{
MyCustomSettings myCustomSettings;
public MyController(IOptions<MyCustomSettings> myCustomSettings)
{
this.myCustomSettings = myCustomSettings.Value;
}
public ActionResult SomeAction(...)
{
var config = myCustomSettings;
}
}
If you are accessing the setting from a repository method you need to inject the configuration to the service endpoint (controller) using one of the methods shown above, and pass the setting to the constructor / method of your repository:
public class MyController : ServiceEndpoint
{
public ServiceResponse SomeServiceCall(MyRequest serviceRequest,
[FromServices] IOptions<MyCustomSettings> myCustomSettings)
{
return new MyRepository().SomeServiceCall(serviceRequest, myCustomSettings)
}
}
public class MyRepository
{
public ServiceResponse SomeServiceCall(MyRequest serviceRequest,
IOptions<MyCustomSettings> myCustomSettings)
{
// use myCustomSettings here
}
}
If you are accessing the setting from a view, and it is not possible to pass it from controller (layout page?), inject it like:
@model MyModel
@using Microsoft.Extensions.Options
@inject IOptions<MyCustomSettings> myCustomSettings
<div>
@if (myCustomSettings?.Value.SomeFlag == true)
{
}
...
</div>
Fixing Config.Get
in DataExplorerEndpoint.cs
If you have DataExplorer sample in your StartSharp based project, do following steps in
DataExplorerEndpoint.cs
:
You may also get latest DataExplorerEndpoint.cs from StartSharp repository.
All replaces is given as regex as before, but should be applied only to the specified file.
- Add
using Microsoft.Extensions.Options
afterusing Microsoft.AspNetCore.Mvc;
- Replace
Request request\)
withRequest request, [FromServices] IOptions<DataExplorerConfig> options
- Replace
Config\.Get<DataExplorerConfig>\(\)
withoptions.Value
- Replace
ListConnections\(new ListRequest\(\)\)
withListConnections(new ListRequest(), options)
- Replace
ConnectionKey \}\)
withConnectionKey }, options)
Fixing Config.Get
in BackgroundJobManager.cs
If you have BackgroundJobManager
in your project, do following in BackgroundJobManager.cs
:
You may also get latest BackgroundJobManager.cs from StartSharp repository.
- Add
using Microsoft.Extensions.Options;
andusing Serenity.Abstractions;
- Replace
static
with empty string. - Replace
public class BackgroundJobManager
withpublic class BackgroundJobManager : IBackgroundJobManager
- Add
private IExceptionLogger logger;
afterprivate bool isDisabled;
- Replace
BackgroundJobManager\(\)
withpublic BackgroundJobManager(IOptions<BackgroundJobSettings> options, IExceptionLogger logger = null)
- Replace
Config.Get\<BackgroundJobSettings\>\(\)\.Enabled;
withoptions.Value.Enabled;
and addthis.logger = logger;
line after that. - Replace
ex.Log\(\)
withex.Log(logger)
Add a IBackgroundJobManager.cs
file with following content next to BackgroundJobManager.cs
, replacing the namespace with yours:
namespace YourNamespace.Common.Services
{
public interface IBackgroundJobManager
{
void Initialize();
void Register(IBackgroundJob task);
void Reset();
}
}
Edit Startup.cs
and do following steps:
Add
services.AddSingleton<IBackgroundJobManager, BackgroundJobManager>();
afterservices.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
line.Add
var backgroundJobManager = app.ApplicationServices.GetRequiredService<IBackgroundJobManager>();
before the firstBackgroundManager.Register(...)
line.Replace
BackgroundJobManager\.
withbackgroundJobManager.
Replacing EmailHelper with a IEmailSender Interface
We used to have an EmailHelper static class which makes testing difficult, so it will be replaced with an abstraction.
Delete Modules/Common/EmailHelper.cs
file and create a Modules/Common/EmailSender
folder with following files in it (replace namespace with your project namespace):
Warning! If you made any modifications to EmailHelper.cs, you need to apply your changes to new style manually
- IEmailSender.cs:
using MimeKit;
namespace YourProject.Common
{
public interface IEmailSender
{
void Send(MimeMessage message, bool skipQueue = false);
}
}
- SmtpSettings.cs:
namespace YourProject.Common
{
public class SmtpSettings
{
public const string SectionKey = "SmtpSettings";
public string Host { get; set; }
public int Port { get; set; }
public bool UseSsl { get; set; }
public string From { get; set; }
public string PickupPath { get; set; }
}
}
- EmailSender.cs:
using MailKit.Net.Smtp;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Options;
using MimeKit;
using Serenity.Data;
using YourProject.Common.Entities;
using YourProject.Common.Services;
using System;
using System.IO;
namespace YourProject.Common
{
public class EmailSender : IEmailSender
{
private IWebHostEnvironment host;
private SmtpSettings smtp;
// these three lines below are only for StartSharp with mail queue
private MailingServiceSettings mailing;
private ISqlConnections connections;
private IUserAccessor userAccessor;
public EmailSender(IWebHostEnvironment host, IOptions<SmtpSettings> smtp,
// below line is only for StartSharp with mail queue
IOptions<MailingServiceSettings> mailing, ISqlConnections connections, IUserAccessor userAccessor)
{
this.host = (host ?? throw new ArgumentNullException(nameof(host)));
this.smtp = (smtp ?? throw new ArgumentNullException(nameof(smtp))).Value;
// three lines below only for StartSharp with mail queue
this.mailing = (mailing ?? throw new ArgumentNullException(nameof(mailing))).Value;
this.connections = connections ?? throw new ArgumentNullException(nameof(connections));
this.userAccessor = userAccessor ?? throw new ArgumentNullException(nameof(userAccessor));
}
public void Send(MimeMessage message, bool skipQueue)
{
if (message == null)
throw new ArgumentNullException(nameof(message));
if (message.From.Count == 0 && !string.IsNullOrEmpty(smtp.From))
message.From.Add(MailboxAddress.Parse(smtp.From));
// this first if block is only for StartSharp with mail queue
if (!skipQueue && mailing.AutoUse)
{
using (var connection = connections.NewFor<MailRow>())
new MailingService().Enqueue(connection, message, userAccessor);
}
else if (!string.IsNullOrEmpty(smtp.Host))
{
using var client = new SmtpClient();
client.Connect(smtp.Host, smtp.Port, smtp.UseSsl);
client.Send(message);
client.Disconnect(true);
}
else
{
var pickupPath = string.IsNullOrEmpty(smtp.PickupPath) ?
Path.Combine(host.ContentRootPath, "App_Data", "Mail") :
Path.Combine(host.ContentRootPath, smtp.PickupPath);
if (!Directory.Exists(pickupPath))
Directory.CreateDirectory(pickupPath);
message.WriteTo(Path.Combine(pickupPath, DateTime.Now.ToString("yyyyMMdd_HHmmss_fff") + ".eml"));
}
}
}
}
- EmailSenderExtensions.cs:
using MimeKit;
using System;
namespace YourProject.Common
{
public static class EmailSenderExtensions
{
public static void Send(this IEmailSender emailSender, string subject, string body, string mailTo)
{
var message = new MimeMessage();
if (mailTo == null)
throw new ArgumentNullException(nameof(mailTo));
message.To.Add(MailboxAddress.Parse(mailTo));
message.Subject = subject;
var bodyBuilder = new BodyBuilder
{
HtmlBody = body
};
message.Body = bodyBuilder.ToMessageBody();
emailSender.Send(message);
}
}
}
- Register IEmailSender as Singleton Service in Startup.cs:
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<Common.IEmailSender, Common.EmailSender>();
Fixing MailingService
If you have StartSharp mail queue feature, do following changes:
- Add
using Serenity.Abstractions;
- Add a
IUserAccessor
parameter toEnqueue
method:
public void Enqueue(IDbConnection connection, MimeMessage message, IUserAccessor userAccessor, Guid? uid = null)
{
//...
mail.InsertUserId = userAccessor?.User.GetIdentifier() == null ? (int?)null :
Convert.ToInt32(userAccessor?.User.GetIdentifier());
//...
}
- Add two parameters to
SendById
method:
public bool SendById(IDbConnection connection, long mailId, IEmailSender emailSender, int retryLimit)
- Change the try catch block inside
SendById
method like this:
try
{
var message = BuildMessage(mail);
emailSender.Send(message);
}
catch (Exception ex)
{
errorMessage = ex.Message;
status = retryCount < retryLimit ? MailStatus.InQueue : MailStatus.Failed;
}
Fixing MailingBackgroundJob
If you have StartSharp mailing background job, do following changes in MailingBackgroundJob.cs
:
- Add
using Serenity.Abstractions;
andusing Microsoft.Extensions.Options;
- Add following fields and constructor:
public class MailingBackgroundJob : PeriodicBackgroundJob
{
private readonly ISqlConnections connections;
private readonly IEmailSender emailSender;
private readonly MailingServiceSettings config;
private readonly IExceptionLogger logger;
public MailingBackgroundJob(ISqlConnections connections, IEmailSender emailSender,
IOptions<MailingServiceSettings> options, IExceptionLogger logger = null)
{
this.connections = connections ?? throw new ArgumentNullException(nameof(connections));
this.emailSender = emailSender ?? throw new ArgumentNullException(nameof(emailSender));
config = (options ?? throw new ArgumentNullException(nameof(options))).Value;
this.logger = logger;
}
- Remove two
var config = Config.Get<MailingServiceSettings>();
lines - Replace
SqlConnections.NewFor
withconnections.NewFor
- Replace
ex.Log()
withex.Log(logger)
- Replace
new MailingService().SendById(connection, mail.MailId.Value);
withnew MailingService().SendById(connection, mail.MailId.Value, emailSender, config.RetryLimit);
Edit Startup.cs
:
- Replace
new MailingBackgroundJob()
withActivatorUtilities.CreateInstance<MailingBackgroundJob>(app.ApplicationServices)
Removing Serenity.Configuration
Namespace
No such namespace exists anymore, so we need to delete any references to it:
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
using Serenity\.Configuration\;[\t ]*\r?\n
inFind
inputClear value in
Replace
inputClick
Replace All
Replace Serenity.Extensibility
Namespace with Serenity.ComponentModel
NestedPermissions
and NestedLocalTexts
in Serenity.Extensibility
namespace are moved to Serenity.ComponentModel
namespace.
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
using Serenity\.Extensibility\;(\t ]*\r?\n)
inFind
inputType
using Serenity.ComponentModel$1
inReplace
inputClick
Replace All
There might be double usings for Serenity.ComponentModel after this in some files like Startup.cs
, please remove one.
Using BaseRepository
and IRequestContext
in Repositories
We have a new IRequestContext
class declared like below which you can inject into your repositories, handlers, controllers and other kinds of classes.
public interface IRequestContext
{
IBehaviorProvider Behaviors { get; }
ITwoLevelCache Cache { get; }
ITextLocalizer Localizer { get; }
IPermissionService Permissions { get; }
ClaimsPrincipal User { get; }
}
This interface simply contains references to minimum set of services that repositories, request handlers and endpoints might need in a Serenity application:
- Behaviors: Provides access to service behaviors, mostly used by request handlers
- Cache: ITwoLevelCache instance which also provides access to MemoryCache and DistributedCache services
- Localizer: Text localizer (required for LocalText to string conversion)
- Permissions: Access to permission service
- User: Provides access to current user (HttpContext.User in web contexts)
We could inject these services into your repositories / handlers separately but this design allows us to add new services to this interface in the future if required without breaking your code. As .NET dependency injection only supports Constructor Injection by default, if we had to add these as separate constructor injection arguments, you would have to change all your derived constructors if we decide to add a new required interface to our base constructors.
We also created a base repository class that accepts a IRequestContext parameter and provides access to these services as properties of itself.
public class BaseRepository
{
public BaseRepository(IRequestContext context)
{
Context = context ?? throw new ArgumentNullException(nameof(context));
}
protected IRequestContext Context { get; }
protected ITextLocalizer Localizer => Context.Localizer;
protected IPermissionService Permissions => Context.Permissions;
protected ClaimsPrincipal User => Context.User;
}
We are now going to set this as base class for all your existing repositories:
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)public[\t ]+class[\t ]+([A-Za-z0-9_]*)Repository[\t ]*(\r?\n|)[\t ]*\{
inFind
inputType
$1public class $2Repository : BaseRepository$3$1{$3$1$1public $2Repository(IRequestContext context)$3$1$1$1 : base(context)$3$1$1{$3$1$1}$3
inReplace
inputClick
Replace All
Handling UserRepository
If your UserRepository is a partial class it will not be handled by previous regex, so need to manually add the constructor:
Partial repositories might have multiple files and including them in previous regex would create multiple constructors, which would be invalid.
Open UserRepository.cs
and modify it like below:
public partial class UserRepository : BaseRepository
{
public UserRepository(IRequestContext context)
: base(context)
{
}
// ...
}
Adding a constructor that accepts IRequestContext
to Request Handlers
Just like we added a constructor that accepts IRequestContext for repositories, we need to do it for RequestHandlers.
You should have classes like below in your repositories:
private class MySaveHandler : SaveRequestHandler<MyRow>
{
}
They now need a constructor with IRequestContext parameter as all base request handlers have one.
private class MySaveHandler : SaveRequestHandler<MyRow>
{
public MySaveHandler(IRequestContext context)
: base(context)
{
}
}
First do it for handlers that are not empty:
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)(private|public)[\t ]+class[\t ]+My(Save|Retrieve|List|Delete|Undelete)Handler[\t ]*\:[\t ]*(\3)RequestHandler(\<[A-Za-z\.\, \t]+\>)[\t ]*\{?[\t ]*(\r?\n)[\t\r\s]*\{
inFind
inputType
in$1$2 class My$3Handler : $3RequestHandler$5$6$1{$6$1 public My$3Handler(IRequestContext context)$6$1$1 : base(context)$6$1 {$6$1 }$6
Replace
inputClick
Replace All
Now will repeat it for empty handlers:
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
([\t ]*)(private|public)[\t ]+class[\t ]+My(Save|Retrieve|List|Delete|Undelete)Handler[\t ]*\:[\t ]*(\3)RequestHandler(\<[A-Za-z\.\, \t]+\>)[\t ]*\{?[\t ]*\}[\t ]*(\r?\n)([\t\r\s]*\n)?
inFind
inputType
in$1$2 class My$3Handler : $3RequestHandler$5$6$1{$6$1 public My$3Handler(IRequestContext context)$6$1$1 : base(context)$6$1 {$6$1 }$6$1}$6$6
Replace
inputClick
Replace All
Pass Context
to Handler Constructors
As all handlers now requires a context
parameter, need to pass it to them, so all calls like this:
new MySaveHandler().Process(uow, request, SaveRequestType.Create);
will be replaced with:
new MySaveHandler(Context).Process(uow, request, SaveRequestType.Create);
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
new[\t ]*My(Save|Retrieve|List|Delete|Undelete)Handler[\t ]*\([\t ]*\)
Type
innew My$1Handler(Context)
Replace
inputClick
Replace All
Passing Context
to Repository Constructors
As all repositories now requires a context
parameter, need to pass it to them, so all calls like this:
new MyRepository().Create(uow, request);
will be replaced with:
new MyRepository(Context).Create(uow, request);
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
new[\t ]*([A-Za-z0-9_]*)Repository[\t ]*\([\t ]*\)
Type
innew $1Repository(Context)
Replace
inputClick
Replace All
Getting IRequestContext
Instance in Ordinary Pages
Service endpoints deriving from ServiceEndpoint
base class (XyzEndpoint.cs) automatically has a reference to Context
so the previous change we made for repository constructors will work with them, but ordinary controllers don't have that property.
So you'll need to use constructor injection / [FromServices] attribute like we did for options if you are creating a repository in any of Page.cs files:
public ActionResult MyAction([FromServices] IRequestContext context)
{
// ...
var result = new MyRepository(context).Something();
// ...
}
Fixing LoggingRow and Derived Rows
If you are using LoggingRow
sample:
public abstract class LoggingRow : Row<LoggingRow.RowFields>, ILoggingRow
{
protected LoggingRow(RowFieldsBase fields)
: base(fields)
{
Modify it like below:
public abstract class LoggingRow<TFields> : Row<TFields>, ILoggingRow
where TFields : LoggingRowFields
{
protected LoggingRow(TFields fields) : base(fields) { }
protected LoggingRow() : base() { }
[NotNull, Insertable(false), Updatable(false)]
public Int32? InsertUserId
{
get => fields.InsertUserId[this];
set => fields.InsertUserId[this] = value;
}
[NotNull, Insertable(false), Updatable(false)]
public DateTime? InsertDate
{
get => fields.InsertDate[this];
set => fields.InsertDate[this] = value;
}
[Insertable(false), Updatable(false)]
public Int32? UpdateUserId
{
get => fields.UpdateUserId[this];
set => fields.UpdateUserId[this] = value;
}
[Insertable(false), Updatable(false)]
public DateTime? UpdateDate
{
get => fields.UpdateDate[this];
set => fields.UpdateDate[this] = value;
}
Field IInsertLogRow.InsertUserIdField => fields.InsertUserId;
Field IUpdateLogRow.UpdateUserIdField => fields.UpdateUserId;
DateTimeField IInsertLogRow.InsertDateField => fields.InsertDate;
DateTimeField IUpdateLogRow.UpdateDateField => fields.UpdateDate;
}
public class LoggingRowFields : RowFieldsBase
{
public Int32Field InsertUserId;
public DateTimeField InsertDate;
public Int32Field UpdateUserId;
public DateTimeField UpdateDate;
public LoggingRowFields(string tableName = null, string fieldPrefix = null)
: base(tableName, fieldPrefix)
{
}
}
And if you have a row that derives from LoggingRow
:
public class SomeRow : LoggingRow, IIdRow
{
public class SomeFields : LoggingRow.LoggingRowFields
{
}
}
It should become:
public class SomeRow : LoggingRow<SomeRow.RowFields>, IIdRow
{
public class RowFields : LoggingRowFields
{
}
}
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
class\s*([A-Za-z0-9_]*)Row\s*:\s*([A-Za-z0-9_.]*)LoggingRow\s*[^<]
Type
inclass $1Row : $2LoggingRow<$1Row.RowFields>,
Replace
inputClick
Replace All
Type
class\s*RowFields\s*:\s*([a-zA-Z0-9_.]*)LoggingRow\.LoggingRowFields
inFind
inputType
inclass RowFields : $1LoggingRowFields
Replace
inputClick
Replace All
Replacing IAuthorizationService with IUserAccessor
IAuthorizationService
interface which was used to access current user is removed and needs to be replaced with IUserAccessor
interface which does a similar work but more compatible with ASP.NET Core authentication system.
services.AddSingleton<IAuthorizationService, AuthorizationService>();
AuthorizationService class in under Initialization folder should be removed and you can use Serenity.Web.HttpContextUserAccessor
instead:
services.AddSingleton<IUserAccessor, Serenity.Web.HttpContextUserAccessor>();
If you also need impersonation support, you should use following class instead:
using Microsoft.AspNetCore.Http;
using Serenity.Abstractions;
using Serenity.Web;
using System.Security.Claims;
public class UserAccessor : IUserAccessor, IImpersonator
{
private ImpersonatingUserAccessor impersonator;
public UserAccessor(IHttpContextAccessor httpContextAccessor)
{
impersonator = new ImpersonatingUserAccessor(new HttpContextUserAccessor(httpContextAccessor),
new HttpContextItemsAccessor(httpContextAccessor));
}
public ClaimsPrincipal User => impersonator.User;
public void Impersonate(ClaimsPrincipal user)
{
impersonator.Impersonate(user);
}
public void UndoImpersonate()
{
impersonator.UndoImpersonate();
}
}
// Startup.cs
services.AddSingleton<IUserAccessor, UserAccessor>();
Remove References to Dependency
We use .NET dependency injection and there is no longer a Dependency
class, so remove this line in Startup.cs
:
Dependency.SetResolver(new DependencyResolver(app.ApplicationServices));
And remove DependencyResolver
class under Initialization
folder.
Also if you use Dependency.Resolve
anywhere, replace those calls with constructor injection, or [FromServices] attribute.
Replacing SelfAssemblies with ITypeSource
Serenity apps used to hold a ExtensibilityHelper.SelfAssemblies
array, which was used to find types used by Serenity like forms, lookup scripts etc. but it is now removed and will be replaced with an ITypeSource
abstraction.
So given the code below in Startup.cs:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILoggerFactory loggerFactory, IAntiforgery antiforgery)
{
Serenity.Extensibility.ExtensibilityHelper.SelfAssemblies =
new System.Reflection.Assembly[]
{
typeof(LocalTextRegistry).Assembly,
typeof(SqlConnections).Assembly,
typeof(Row).Assembly,
typeof(SaveRequestHandler<>).Assembly,
typeof(WebSecurityHelper).Assembly,
typeof(Startup).Assembly
};
//...
}
It should be moved to start of ConfigureServices method and replaced with a ITypeSource service registration.
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<ITypeSource>(new DefaultTypeSource(new[]
{
typeof(LocalTextRegistry).Assembly,
typeof(ISqlConnections).Assembly,
typeof(IRow).Assembly,
typeof(SaveRequestHandler<>).Assembly,
typeof(IDynamicScriptManager).Assembly,
typeof(Startup).Assembly
}));
Fixing Local Text Initialization
Text registration block in Startup.cs:
public static void InitializeLocalTexts(ILocalTextRegistry textRegistry,
IWebHostEnvironment env)
{
textRegistry.AddNestedTexts();
textRegistry.AddNestedPermissions();
textRegistry.AddEnumTexts();
textRegistry.AddRowTexts();
textRegistry.AddJsonTexts(Path.Combine(env.WebRootPath,
"Scripts/serenity/texts".Replace('/', Path.DirectorySeparatorChar)));
textRegistry.AddJsonTexts(Path.Combine(env.WebRootPath,
"Scripts/site/texts".Replace('/', Path.DirectorySeparatorChar)));
textRegistry.AddJsonTexts(Path.Combine(env.ContentRootPath,
"App_Data/texts".Replace('/', Path.DirectorySeparatorChar)));
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILoggerFactory loggerFactory, IAntiforgery antiforgery)
{
var textRegistry = app.ApplicationServices.GetRequiredService<ILocalTextRegistry>();
InitializeLocalTexts(textRegistry, env);
//...
should be replaced with this:
public static void InitializeLocalTexts(IServiceProvider services)
{
var env = services.GetRequiredService<IWebHostEnvironment>();
services.AddAllTexts(new[]
{
Path.Combine(env.WebRootPath, "Scripts", "serenity", "texts"),
Path.Combine(env.WebRootPath, "Scripts", "site", "texts"),
Path.Combine(env.ContentRootPath, "App_Data", "texts")
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env,
ILoggerFactory loggerFactory, IAntiforgery antiforgery)
{
InitializeLocalTexts(app.ApplicationServices);
// ...
And if you have this line in TranslationRepository.cs
:
public SaveResponse Update(TranslationUpdateRequest request)
{
// ...
Startup.InitializeLocalTexts(localTextRegistry,
Dependency.Resolve<IWebHostEnvironment>());
Replace it with:
public SaveResponse Update(TranslationUpdateRequest request, IServiceProvider services)
{
// ...
Startup.InitializeLocalTexts(services);
And in TranslationEndpoint.cs
:
[HttpPost]
public SaveResponse Update(TranslationUpdateRequest request)
{
return new MyRepository(Context).Update(request, HttpContext.RequestServices);
}
Replacing Old IRequestContext Registration
If you have this line in Startup.cs:
services.AddSingleton<IRequestContext, Serenity.Web.RequestContext>();
Replace it with:
services.AddSingleton<IHttpContextItemsAccessor, HttpContextItemsAccessor>();
If you don't have that line but used a UserAccessor with impersonation support you still need to add it before registering the user accessor:
services.AddSingleton<IHttpContextItemsAccessor, HttpContextItemsAccessor>();
services.AddSingleton<IUserAccessor, Administration.UserAccessor>();
Fixing BatchGenerationUpdater Calls
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Look in
is Current project,Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is checked.Type
BatchGenerationUpdater\.OnCommit\((uow|this\.UnitOfWork|UnitOfWork),\s*(.*).GenerationKey\)
Type
Cache.InvalidateOnCommit($1, $2)
inReplace
inputClick
Replace All
Replacing Authorization.HasPermission Calls
Static Authorization class is removed and calls like below:
Authorization.HasPermission("SomePermission")
with:
Permissions.HasPermission("SomePermission")
where Permissions is a reference to IPermissionService
.
Handlers, repositories and service endpoints already has this property, but if you need it somewhere else (e.g. ordinary Controller), you need to use constructor injection / [FromServices] attribute.
public ActionResult MyAction(...
[FromServices] IPermissionService permissions)
{
permissions.HasPermission("SomePermission");
}
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
Authorization\.HasPermission\(([A-Za-z\."]*)\)
inFind
inputType
Permissions.HasPermission($1)
inReplace
inputClick
Replace All
Replacing Authorization.ValidatePermission Calls
Static Authorization class is removed and calls like below:
Authorization.ValidatePermission("SomePermission")
with:
Permissions.ValidatePermission("SomePermission")
where Permissions is a reference to IPermissionService
while Localizer is a reference to ITextLocalizer
.
Handlers, repositories and service endpoints already has these properties, but if you need it somewhere else (e.g. ordinary Controller), you need to use constructor injection / [FromServices] attribute like this:
public ActionResult MyAction(...
[FromServices] IPermissionService permissions, [FromServices] ITextLocalizer localizer)
{
permissions.ValidatePermission("SomePermission", localizer);
}
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
Authorization\.ValidatePermission\(([^\r\n]+)\);
inFind
inputType
Permissions.ValidatePermission($1, Localizer);
inReplace
inputClick
Replace All
Type
Serenity\.Permissions
inFind
inputType
Permissions.ValidatePermission($1, Localizer);
inReplace
inputClick
Replace All
Replacing TwoLevelCache.ExpireGroupItems Calls
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
TwoLevelCache\.ExpireGroupItems
inFind
inputType
Cache.ExpireGroupItems
inReplace
inputClick
Replace All
Replacing TwoLevelCache.Get and TwoLevelCache.GetLocalStoreOnly Calls
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
TwoLevelCache\.Get
inFind
inputType
Cache.Get
inReplace
inputClick
Replace All
Replacing TwoLevelCache.Remove Calls
Type
TwoLevelCache\.Remove
inFind
inputType
Cache.Remove
inReplace
inputClick
Replace All
Remove DetailListSaveHandler
There was a DetailListSaveHandler
class that was used before MasterDetailRelation behavior was written, so you should delete Modules/Common/Helpers/DetailListSaveHandler.cs
Replacing IdField Indexers and NotesBehavior.cs
As ID fields can now be any field, and there is no IIdField interface anymore, you should replace idField[row] calls with idField.AsObject.
Such an access is available in NotesBehavior (note that Notes behavior still expects integer ID fields).
Edit Modules/Northwind/Note/NotesBehavior.cs
:
- Add following constructor and properties to
NotesBehavior
:
public IRequestContext Context { get; }
public ISqlConnections SqlConnections { get; }
public NotesBehavior(IRequestContext context, ISqlConnections sqlConnections)
{
Context = context ??
throw new ArgumentNullException(nameof(context));
SqlConnections = sqlConnections ??
throw new ArgumentNullException(nameof(sqlConnections));
}
Replace
idField\[handler\.Row\](\s)
withidField.AsObject(handler.Row)$1
Replace
([A-Za-z]*dField)\[([A-Za-z\.]*)]\.Value
withConvert.ToInt64($1.AsObject($2))
Replace
rowIdField\[item\]
withrowIdField.AsObject(item)
Replace
id\.Value
withConvert.ToInt64(id)
Fixing UserPermissionService
Take latest version of PermissionService.cs from StartSharp / Serene repository, or do following changes:
- Add following constructor and properties:
protected ITwoLevelCache Cache { get; }
protected ISqlConnections SqlConnections { get; }
public ITypeSource TypeSource { get; }
protected IUserAccessor UserAccessor { get; }
public PermissionService(ITwoLevelCache cache, ISqlConnections sqlConnections,
ITypeSource typeSource, IUserAccessor userAccessor)
{
Cache = cache ?? throw new ArgumentNullException(nameof(cache));
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
TypeSource = typeSource ?? throw new ArgumentNullException(nameof(typeSource));
UserAccessor = userAccessor ?? throw new ArgumentNullException(nameof(userAccessor));
}
- Change start of HasPermission method like this:
public bool HasPermission(string permission)
{
if (Authorization.Username == "admin")
if (permission == null)
return false;
if (permission == "*")
return true;
var user = (UserDefinition)Authorization.UserDefinition;
if (user == null)
var isLoggedIn = UserAccessor.IsLoggedIn();
if (permission == "?")
return isLoggedIn;
if (!isLoggedIn)
return false;
var username = UserAccessor.User?.Identity?.Name;
if (username == "admin")
return true;
var userId = Convert.ToInt32(UserAccessor.User.GetIdentifier());
//...
// only admin has Impersonation...
Replace
user\.UserId
withuserId
If have implicit permissions feature (StartSharp) replace
new Repositories\.UserPermissionRepository\(\)\.ImplicitPermissions
withUserPermissionRepository.GetImplicitPermissions(Cache.Memory, TypeSource);
and change the code for ImplicitPermissions property inUserPermissionRepository
:
public static Dictionary<string, HashSet<string>> GetImplicitPermissions(
IMemoryCache memoryCache, ITypeSource typeSource)
{
// ...
}
- Replace
LocalCache\.Get
withmemoryCache.Get
in GetImplicitPermissions - Replace the code block that enumerates ExtensibilityHelper.SelfAssemblies like
foreach (var type in typeSource.GetTypesWithAttribute(
typeof(NestedPermissionKeysAttribute)))
{
addFrom(type);
}
You may also get latest code from StartSharp
Handling LocalText to String Conversion
We were able to simply pass a LocalText object to any method that requires a string, and it was automatically translated:
String.Format(Texts.Validation.MinRequiredPasswordLength, 5)
Unfortunately that is no longer possible as translation requires a localization context:
String.Format(Texts.Validation.MinRequiredPasswordLength.ToString(Localizer), 5);
Localizer is an ITextLocalizer object which is available in repositories, and service endpoints but should be injected in other contexts.
Open
Replace in Files
dialog in Visual StudioCtrl+Shift+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
Texts\.([A-Za-z_.]*)([\s,;\<\r\n)])
inFind
inputType
Texts.$1.ToString(Localizer)$2
inReplace
inputMake sure
File Types
is*.cshtml; *.cs
Click
Replace All
Type
Texts\.([A-Za-z_.]*)\.ToString\(\)
inFind
inputType
Texts.$1.ToString(Localizer)
inReplace
inputClick
Replace All
Handling LocalText.Get Calls
All calls like following:
LocalText.Get("SomeKey")
LocalText.TryGet("SomeKey")
should be replaced with
Localizer.Get("SomeKey")
Localizer.TryGet("SomeKey")
Type
(Serenity\.)?LocalText\.(Get|TryGet)\((["A-Za-z_\.]+)\)
inFind
inputType
Localizer.$2($3)
inReplace
inputMake sure
File Types
is*.cshtml; *.cs
Click
Replace All
Injecting Localizer to CSHTML Files
Find all .CSHTML files which needs Localizer
service to get injected with searching following regex in *.cshtml files:
.*(?Localizer\.(Try|Get))
Add this line after @model
directive if exists, or as the first line:
@model XYZ
@inject Serenity.ITextLocalizer Localizer
Inject Localizer to AccountPage
Add this property and constructor to AccountController.cs
as Localizer is required in many files:
protected ITextLocalizer Localizer { get; }
public AccountController(ITextLocalizer localizer)
{
Localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
}
Fixing TranslationRepository and TranslationEndpoint
Get them from latest StartSharp / Serene repo or do following changes:
- Modify TranslationEndpoint like below:
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Serenity.Abstractions;
using Serenity.Services;
using System;
using MyRepository = StartSharp.Administration.Repositories.TranslationRepository;
namespace StartSharp.Administration.Endpoints
{
[Route("Services/Administration/Translation/[action]")]
[ServiceAuthorize(PermissionKeys.Translation)]
public class TranslationController : ServiceEndpoint
{
protected IWebHostEnvironment HostEnvironment { get; }
protected ILocalTextRegistry LocalTextRegistry { get; }
protected ITypeSource TypeSource { get; }
public TranslationController(IWebHostEnvironment hostEnvironment,
ILocalTextRegistry localTextRegistry, ITypeSource typeSource)
{
HostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
LocalTextRegistry = localTextRegistry ?? throw new ArgumentNullException(nameof(localTextRegistry));
TypeSource = typeSource ?? throw new ArgumentNullException(nameof(typeSource));
}
private MyRepository NewRepository()
{
return new MyRepository(Context, HostEnvironment, LocalTextRegistry, TypeSource);
}
public ListResponse<TranslationItem> List(TranslationListRequest request)
{
return NewRepository().List(request);
}
[HttpPost]
public SaveResponse Update(TranslationUpdateRequest request)
{
return NewRepository().Update(request, HttpContext.RequestServices);
}
}
}
- Modify TranslationRepository constructor and GetUserTextsFilePath:
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Newtonsoft.Json.Linq;
//...
protected IWebHostEnvironment HostEnvironment { get; }
protected ILocalTextRegistry LocalTextRegistry { get; }
protected ITypeSource TypeSource { get; }
public TranslationRepository(IRequestContext context, IWebHostEnvironment hostEnvironment,
ILocalTextRegistry localTextRegistry, ITypeSource typeSource)
: base(context)
{
HostEnvironment = hostEnvironment;
LocalTextRegistry = localTextRegistry;
TypeSource = typeSource;
}
public static string GetUserTextsFilePath(IWebHostEnvironment hostEnvironment, string languageID)
{
return Path.Combine(hostEnvironment.ContentRootPath, "App_Data", "texts",
"user.texts." + (languageID.TrimToNull() ?? "invariant") + ".json");
}
- Replace
GetUserTextsFilePath\(targetLanguageID\)
withGetUserTextsFilePath(HostEnvironment, targetLanguageID)
- Replace
JsonConfigHelper\.LoadConfig\<Dictionary\<string, JToken\>\>\(textsFilePath\)
withJSON.Parse<Dictionary<string, JToken>>(File.ReadAllText(textsFilePath))
- Remove
var registry = Dependency.Resolve<ILocalTextRegistry>();
- Replace
GetAllAvailableLocalTextKeys
like following:
public HashSet<string> GetAllAvailableLocalTextKeys()
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (NavigationItemAttribute attr in TypeSource.GetAssemblyAttributes<NavigationItemAttribute>())
result.Add("Navigation." + (attr.Category.IsEmptyOrNull() ? "" : attr.Category + "/") + attr.Title);
foreach (var type in TypeSource.GetTypesWithAttribute(typeof(FormScriptAttribute)))
{
var attr = type.GetAttribute<FormScriptAttribute>();
foreach (var member in type.GetMembers(BindingFlags.Instance | BindingFlags.Public))
{
var category = member.GetCustomAttribute<CategoryAttribute>();
if (category != null && !category.Category.IsEmptyOrNull())
result.Add("Forms." + attr.Key + ".Categories." + category.Category);
}
}
var repository = LocalTextRegistry as LocalTextRegistry;
if (repository != null)
result.AddRange(repository.GetAllTextKeys(false));
return result;
}
- Replace
Dependency\.Resolve\<ILocalTextRegistry\>\(\) as LocalTextRegistry
withLocalTextRegistry as LocalTextRegistry
- Replace
\(localTextRegistry as IRemoveAll\)\?\.RemoveAll\(\)
with(LocalTextRegistry as IRemoveAll)?.RemoveAll()
- Replace
DynamicScriptManager\.Reset\(\)
withservices.GetService<IDynamicScriptManager>()?.Reset()
Handling AccountPage, UserPasswordValidator and Related Code
Find the following line:
services.AddSingleton<IAuthenticationService, Administration.AuthenticationService>();
and replace with
services.AddSingleton<Administration.IUserPasswordValidator, Administration.UserPasswordValidator>();
Find and delete in the following file:
- Modules/Administration/User/Authentication/AuthenticationService.cs
Add new 3 files:
- Modules/Administration/User/Authentication/UserPasswordValidator.cs
namespace StartSharp.Administration
{
public interface IUserPasswordValidator
{
PasswordValidationResult Validate(ref string username, string password);
}
}
- Modules/Administration/User/Authentication/PasswordValidationResult.cs
namespace StartSharp.Administration
{
public enum PasswordValidationResult
{
EmptyUsername,
EmptyPassword,
InactiveUser,
UnknownSource,
Throttle,
DirectoryError,
Invalid,
Valid
}
}
- Modules/Administration/User/Authentication/UserPasswordValidator.cs
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Serenity;
using Serenity.Abstractions;
using Serenity.Data;
using StartSharp.Administration.Entities;
using StartSharp.Administration.Repositories;
using System;
namespace StartSharp.Administration
{
public class UserPasswordValidator : IUserPasswordValidator
{
public UserPasswordValidator(ITwoLevelCache cache, ISqlConnections sqlConnections,
IUserRetrieveService userRetriever,
ILogger<UserPasswordValidator> log = null,
IDirectoryService directoryService = null)
{
Cache = cache ?? throw new ArgumentNullException(nameof(cache));
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
UserRetriever = userRetriever ?? throw new ArgumentNullException(nameof(userRetriever));
DirectoryService = directoryService;
Log = log;
}
protected ITwoLevelCache Cache { get; }
public ISqlConnections SqlConnections { get; }
protected IUserRetrieveService UserRetriever { get; }
protected IDirectoryService DirectoryService { get; }
protected ILogger<UserPasswordValidator> Log { get; }
public PasswordValidationResult Validate(ref string username, string password)
{
if (username.IsTrimmedEmpty())
return PasswordValidationResult.EmptyUsername;
if (password.IsEmptyOrNull())
return PasswordValidationResult.EmptyPassword;
username = username.TrimToEmpty();
if (UserRetriever.ByUsername(username) is UserDefinition user)
return ValidateExistingUser(ref username, password, user);
return ValidateFirstTimeUser(ref username, password);
}
private PasswordValidationResult ValidateExistingUser(ref string username, string password, UserDefinition user)
{
username = user.Username;
if (user.IsActive != 1)
{
Log?.LogError("Inactive user login attempt: {0}", username);
return PasswordValidationResult.InactiveUser;
}
// prevent more than 50 invalid login attempts in 30 minutes
var throttler = new Throttler(Cache.Memory, "ValidateUser:" + username.ToLowerInvariant(), TimeSpan.FromMinutes(30), 50);
if (!throttler.Check())
return PasswordValidationResult.Throttle;
Func<bool> validatePassword = () => UserRepository.CalculateHash(password, user.PasswordSalt)
.Equals(user.PasswordHash, StringComparison.OrdinalIgnoreCase);
if (user.Source == "site" || user.Source == "sign" || DirectoryService == null)
{
if (validatePassword())
{
throttler.Reset();
return PasswordValidationResult.Valid;
}
return PasswordValidationResult.Invalid;
}
if (user.Source != "ldap")
return PasswordValidationResult.UnknownSource;
if (!string.IsNullOrEmpty(user.PasswordHash) &&
user.LastDirectoryUpdate != null &&
user.LastDirectoryUpdate.Value.AddHours(1) >= DateTime.Now)
{
if (validatePassword())
{
throttler.Reset();
return PasswordValidationResult.Valid;
}
return PasswordValidationResult.Invalid;
}
DirectoryEntry entry;
try
{
entry = DirectoryService.Validate(username, password);
if (entry == null)
return PasswordValidationResult.Invalid;
throttler.Reset();
}
catch (Exception ex)
{
Log?.LogError(ex, "Error on directory access");
// couldn't access directory. allow user to login with cached password
if (!user.PasswordHash.IsTrimmedEmpty())
{
if (validatePassword())
{
throttler.Reset();
return PasswordValidationResult.Valid;
}
return PasswordValidationResult.Invalid;
}
throw;
}
try
{
string salt = user.PasswordSalt.TrimToNull();
var hash = UserRepository.GenerateHash(password, ref salt);
var displayName = entry.FirstName + " " + entry.LastName;
var email = entry.Email.TrimToNull() ?? user.Email ?? (username + "@yourdefaultdomain.com");
using (var connection = SqlConnections.NewFor<UserRow>())
using (var uow = new UnitOfWork(connection))
{
var fld = UserRow.Fields;
new SqlUpdate(fld.TableName)
.Set(fld.DisplayName, displayName)
.Set(fld.PasswordHash, hash)
.Set(fld.PasswordSalt, salt)
.Set(fld.Email, email)
.Set(fld.LastDirectoryUpdate, DateTime.Now)
.WhereEqual(fld.UserId, user.UserId)
.Execute(connection, ExpectedRows.One);
uow.Commit();
UserRetrieveService.RemoveCachedUser(Cache, user.UserId, username);
}
return PasswordValidationResult.Valid;
}
catch (Exception ex)
{
Log?.LogError(ex, "Error while updating directory user");
return PasswordValidationResult.Valid;
}
}
private PasswordValidationResult ValidateFirstTimeUser(ref string username, string password)
{
var throttler = new Throttler(Cache.Memory, "ValidateUser:" + username.ToLowerInvariant(), TimeSpan.FromMinutes(30), 50);
if (!throttler.Check())
return PasswordValidationResult.Throttle;
if (DirectoryService == null)
return PasswordValidationResult.Invalid;
DirectoryEntry entry;
try
{
entry = DirectoryService.Validate(username, password);
if (entry == null)
return PasswordValidationResult.Invalid;
throttler.Reset();
}
catch (Exception ex)
{
Log?.LogError(ex, "Error on directory first time authentication");
return PasswordValidationResult.DirectoryError;
}
try
{
string salt = null;
var hash = UserRepository.GenerateHash(password, ref salt);
var displayName = entry.FirstName + " " + entry.LastName;
var email = entry.Email.TrimToNull() ?? (username + "@yourdefaultdomain.com");
username = entry.Username.TrimToNull() ?? username;
using (var connection = SqlConnections.NewFor<UserRow>())
using (var uow = new UnitOfWork(connection))
{
var userId = (int)connection.InsertAndGetID(new UserRow
{
Username = username,
Source = "ldap",
DisplayName = displayName,
Email = email,
PasswordHash = hash,
PasswordSalt = salt,
IsActive = 1,
InsertDate = DateTime.Now,
InsertUserId = 1,
LastDirectoryUpdate = DateTime.Now
});
uow.Commit();
UserRetrieveService.RemoveCachedUser(Cache, userId, username);
}
return PasswordValidationResult.Valid;
}
catch (Exception ex)
{
Log?.LogError(ex, "Error while importing directory user");
return PasswordValidationResult.DirectoryError;
}
}
}
}
Apply the following file changes in StartSharp:
or following file changes in Serene:
Sergen Tool Update
Open command line in your project directory and run following:
dotnet tool update sergen
Fix SmtpSettings
.NET options does not like converting nulls to default values when the target is not nullable so fix the SmtpSettings in appsettings.json file (port and usessl, and pickuppath parts) like following (or apply your valid settings):
"SmtpSettings": {
"Host": null,
"Port": null,
"UseSsl": null,
"From": null
"Port": 25,
"UseSsl": false,
"From": null,
"PickupPath": "App_Data/Mail"
}
Handling DataMigrations.cs
We are going to use FluentMigrator's latest version and making DataMigrations dependency injectable.
- Add following usings:
using FluentMigrator.Runner;
using FluentMigrator.Runner.Conventions;
using FluentMigrator.Runner.Processors;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.DependencyInjection;
using System.Data.Common;
Remove
using System.Data.SqlClient;
Replace
public static class DataMigrations
withpublic class DataMigrations : IDataMigrations
Replace
SqlConnections\.GetConnectionString\(databaseKey\)\;
with
SqlConnections.TryGetConnectionString(databaseKey);
if (cs == null)
throw new ArgumentOutOfRangeException(nameof(databaseKey));
- Replace
Serenity\.Dependency\.Resolve\<IWebHostEnvironment\>\(\)\.ContentRootPath
withHostEnvironment.ContentRootPath
- Replace
cs\.ProviderFactory\.CreateConnectionStringBuilder\(\)\;
withDbProviderFactories.GetFactory(cs.ProviderName).CreateConnectionStringBuilder();
- Replace
, cs.ProviderName)
with, cs.ProviderName, cs.Dialect)
- Replace
var hostingEnvironment \= Serenity\.Dependency\.TryResolve\<IWebHostEnvironment\>\(\)\;
withvar hostingEnvironment = HostEnvironment;
- Remove
var connection = cs.ConnectionString;
line - Remove
bool isSqlServer = serverType.StartsWith(""SqlServer"", StringComparison.OrdinalIgnoreCase);
line - Find the line starting with
using (var sw = new StringWriter
and modify remaining lines like below:
var conventionSet = new DefaultConventionSet(defaultSchemaName: null,
Path.GetDirectoryName(typeof(DataMigrations).Assembly.Location));
var serviceProvider = new ServiceCollection()
.AddLogging(lb => lb.AddFluentMigratorConsole())
.AddFluentMigratorCore()
.AddSingleton<IConventionSet>(conventionSet)
.Configure<TypeFilterOptions>(options =>
{
options.Namespace = "!!!YourProjectNamespace!!!.Migrations." + databaseKey + "DB";
})
.Configure<ProcessorOptions>(options =>
{
options.Timeout = TimeSpan.FromSeconds(90);
})
.ConfigureRunner(builder =>
{
if (databaseType == OracleDialect.Instance.ServerType)
builder.AddOracleManaged();
else if (databaseType == SqliteDialect.Instance.ServerType)
builder.AddSQLite();
else if (databaseType == FirebirdDialect.Instance.ServerType)
builder.AddFirebird();
else if (databaseType == MySqlDialect.Instance.ServerType)
builder.AddMySql5();
else if (databaseType == PostgresDialect.Instance.ServerType)
builder.AddPostgres();
else
builder.AddSqlServer();
builder.WithGlobalConnectionString(cs.ConnectionString);
builder.WithMigrationsIn(typeof(DataMigrations).Assembly);
})
.BuildServiceProvider();
var culture = CultureInfo.CurrentCulture;
try
{
if (isFirebird)
Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture;
using var scope = serviceProvider.CreateScope();
var runner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
runner.MigrateUp();
}
catch (Exception ex)
{
throw new InvalidOperationException("Error executing migration!", ex);
}
finally
{
if (isFirebird)
Thread.CurrentThread.CurrentCulture = culture;
}
- Edit Your .CSPROJ file and replace following line:
<PackageReference Include="Serenity.FluentMigrator.Runner" Version="1.6.904" />
with
<PackageReference Include="FluentMigrator.Runner" Version="3.2.9" />
- Edit Startup.cs and following lines in ConfigureServices:
services.AddSingleton<IDataMigrations, DataMigrations>();
services.AddSingleton<IReportRegistry, ReportRegistry>();
- Replace following lines:
services.AddConfig(Configuration);
services.AddCaching();
services.AddAnnotationTypes();
services.AddTextRegistry();
services.AddFileLogging();
services.AddSingleton<IAuthenticationService, Administration.AuthenticationService>();
with
services.AddServiceHandlers();
services.AddDynamicScripts();
services.AddCssBundling();
services.AddScriptBundling();
services.AddUploadStorage();
services.AddSingleton<Administration.IUserPasswordValidator, Administration.UserPasswordValidator>();
- Edit Startup.cs and replace following line
DataMigrations.Initialize();
with
app.ApplicationServices.GetRequiredService<IDataMigrations>().Initialize();
add these usings to start:
using Microsoft.Data.SqlClient;
using Serenity.Reporting;
using System.Data.Common;
remove this usings:
using System.Data.SqlClient;
using Serenity.Web.Middleware;
and add following to RegisterDataProviders:
DbProviderFactories.RegisterFactory("Microsoft.Data.SqlClient", SqlClientFactory.Instance);
Fix SqlErrorStore
Add
using System.Data.Common;
Remove these lines:
private readonly string connectionString;
private readonly bool isSqlServer;
- Modify constructor like this:
public SqlErrorStore(ErrorStoreSettings settings, string providerName)
: base(settings)
{
if (settings == null)
throw new ArgumentNullException(nameof(settings));
displayCount = Math.Min(displayCount, MaximumDisplayCount);
this.providerName = providerName ?? throw new ArgumentNullException(nameof(providerName));
}
Replace
if (isSqlServer)
withif (providerName.IndexOf("SqlClient") >= 0)
Modify GetConnection:
private IDbConnection GetConnection()
{
var connection = DbProviderFactories.GetFactory(providerName).CreateConnection();
connection.ConnectionString = Settings.ConnectionString;
return connection;
}
Fixing Startup.ExceptionLog
Modify Log method like this:
public void Log(Exception exception, string category = null)
{
exception.Log(httpContextAccessor.HttpContext, category);
}
Removing Web Based Sergen UI
Sergen will soon have its own web based UI, and the sample in the StartSharp/Serene is obsolete.
- Edit
AdministrationNavigation.cs
and remove this line:
[assembly: NavigationLink(9000, "Administration/Sergen", typeof(Administration.SergenController), icon: "fa-magic")]
- Delete
Modules/Administration/Sergen
folder and files under it.
Replacing isPublicDemo References
This is only used for Serenity Demo at https://serenity.is, you may delete any line that looks like following:
UserRepository.isPublicDemo
or replace it with
UserRepository.IsPublicDemo
Fixing DataAuditLog
If you have StartSharp data audit log feature, delete DataAuditLogHandler.cs
and get latest versions of following files from StartSharp repository:
Fixing Row Lookup Scripts
If you have external lookup scripts defined like this:
public LanguageLookup()
You need to add a sqlConnections argument to the constructor (or add the constructor if none exists).
public LanguageLookup(ISqlConnections sqlConnections)
: base(sqlConnections)
Replacing RoleRepository.RoleById by RoleRepository.GetRoleById
Modify static RoleById property like this:
public static Dictionary<int, MyRow> GetRoleById(ITwoLevelCache cache, ISqlConnections sqlConnections)
{
if (cache is null)
throw new ArgumentNullException(nameof(cache));
return cache.GetLocalStoreOnly("RoleById", TimeSpan.Zero,
fld.GenerationKey, () =>
{
if (sqlConnections is null)
throw new ArgumentNullException(nameof(sqlConnections));
using var connection = sqlConnections.NewFor<MyRow>();
return connection.List<MyRow>().ToDictionary(x => x.RoleId.Value);
});
}
Modify TranslationEndpoint.cs
Modify this file like below:
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Serenity.Abstractions;
using Serenity.Services;
using System;
using MyRepository = StartSharp.Administration.Repositories.TranslationRepository;
namespace StartSharp.Administration.Endpoints
{
[Route("Services/Administration/Translation/[action]")]
[ServiceAuthorize(PermissionKeys.Translation)]
public class TranslationController : ServiceEndpoint
{
protected IWebHostEnvironment HostEnvironment { get; }
protected ILocalTextRegistry LocalTextRegistry { get; }
protected ITypeSource TypeSource { get; }
public TranslationController(IWebHostEnvironment hostEnvironment,
ILocalTextRegistry localTextRegistry, ITypeSource typeSource)
{
HostEnvironment = hostEnvironment ?? throw new ArgumentNullException(nameof(hostEnvironment));
LocalTextRegistry = localTextRegistry ?? throw new ArgumentNullException(nameof(localTextRegistry));
TypeSource = typeSource ?? throw new ArgumentNullException(nameof(typeSource));
}
private MyRepository NewRepository()
{
return new MyRepository(Context, HostEnvironment, LocalTextRegistry, TypeSource);
}
public ListResponse<TranslationItem> List(TranslationListRequest request)
{
return NewRepository().List(request);
}
[HttpPost]
public SaveResponse Update(TranslationUpdateRequest request)
{
return NewRepository().Update(request, HttpContext.RequestServices);
}
}
}
Fix TranslationRepository.cs
- Add
using Microsoft.Extensions.DependencyInjection;
- Remove
using System.Web.Hosting;
- Modify constructor and GetUserTextsFilePath as below:
protected IWebHostEnvironment HostEnvironment { get; }
protected ILocalTextRegistry LocalTextRegistry { get; }
protected ITypeSource TypeSource { get; }
public TranslationRepository(IRequestContext context, IWebHostEnvironment hostEnvironment,
ILocalTextRegistry localTextRegistry, ITypeSource typeSource)
: base(context)
{
HostEnvironment = hostEnvironment;
LocalTextRegistry = localTextRegistry;
TypeSource = typeSource;
}
public static string GetUserTextsFilePath(IWebHostEnvironment hostEnvironment, string languageID)
{
return Path.Combine(hostEnvironment.ContentRootPath, "App_Data", "texts",
"user.texts." + (languageID.TrimToNull() ?? "invariant") + ".json");
}
- Replace
GetUserTextsFilePath\(targetLanguageID\)\;
withGetUserTextsFilePath(HostEnvironment, targetLanguageID);
- Replace
GetUserTextsFilePath\(request.TargetLanguageID\)\;
withGetUserTextsFilePath(HostEnvironment, request.TargetLanguageID);
- Replace
var json \= JsonConfigHelper\.LoadConfig\<Dictionary\<string\, JToken\>\>\(textsFilePath\)\;
withvar json = JSON.Parse<Dictionary<string, JToken>>(File.ReadAllText(textsFilePath));
- Remove
var registry = Dependency.Resolve<ILocalTextRegistry>();
line - Replace
registry\.TryGet\((source|target)LanguageID\, key\)
withLocalTextRegistry.TryGet($1LanguageID, key, false)
- Modify GetAllAvailableLocalTexts like this:
public HashSet<string> GetAllAvailableLocalTextKeys()
{
var result = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (NavigationItemAttribute attr in TypeSource.GetAssemblyAttributes<NavigationItemAttribute>())
result.Add("Navigation." + (attr.Category.IsEmptyOrNull() ? "" : attr.Category + "/") + attr.Title);
foreach (var type in TypeSource.GetTypesWithAttribute(typeof(FormScriptAttribute)))
{
var attr = type.GetAttribute<FormScriptAttribute>();
foreach (var member in type.GetMembers(BindingFlags.Instance | BindingFlags.Public))
{
var category = member.GetCustomAttribute<CategoryAttribute>();
if (category != null && !category.Category.IsEmptyOrNull())
result.Add("Forms." + attr.Key + ".Categories." + category.Category);
}
}
var repository = LocalTextRegistry as LocalTextRegistry;
if (repository != null)
result.AddRange(repository.GetAllTextKeys(false));
return result;
}
- Add
(LocalTextRegistry as IRemoveAll)?.RemoveAll();
before `Startup.InitializeLocalTexts(services);`` - Replace
DynamicScriptManager\.Reset\(\)\;
withservices.GetService<IDynamicScriptManager>()?.Reset();
Modify UserRetrieve.cs
Apply the following file changes in StartSharp:
or following file changes in Serene:
Modify UserEndpoint.cs
Find this line:
if (request != null && Serenity.Permissions.HasPermission("ImpersonateAs"))
and change with this line:
if (request != null && Permissions.HasPermission("ImpersonateAs"))
Modify UserRepository.cs
Apply the following file changes in StartSharp:
or following file changes in Serene:
Modify UserPermissionEndpoint.cs
Apply the following file changes:
- Add
using Serenity.Abstractions;
Find this method:
public ListResponse<string> ListPermissionKeys()
{
return new MyRepository(Context).ListPermissionKeys(includeRoles: false);
}
and change with:
public ListResponse<string> ListPermissionKeys(
[FromServices] ISqlConnections sqlConnections,
[FromServices] ITypeSource typeSource)
{
return new MyRepository(Context).ListPermissionKeys(sqlConnections, typeSource, includeRoles: false);
}
Find this method:
public Dictionary<string, HashSet<string>> ListImplicitPermissions()
{
return new MyRepository(Context).ImplicitPermissions;
}
and change with:
public IDictionary<string, HashSet<string>> ListImplicitPermissions(
[FromServices] ITypeSource typeSource)
{
return MyRepository.GetImplicitPermissions(Cache.Memory, typeSource);
}
Modify UserPermissionRepository.cs
Apply the following file changes in StartSharp:
or following file changes in Serene:
Modify AdvanceSamplesPage.cs
Apply the following file changes:
- Add
using Serenity.Data;
- Add
using System;
Find the controller AdvancedSamplesController
and change inside of like in the following:
public AdvancedSamplesController(ISqlConnections sqlConnections)
{
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
}
protected ISqlConnections SqlConnections { get; }
at the end your AdvancedSamplesController
will be like:
[PageAuthorize, Route("AdvancedSamples/[action]")]
public partial class AdvancedSamplesController : Controller
{
public AdvancedSamplesController(ISqlConnections sqlConnections)
{
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
}
protected ISqlConnections SqlConnections { get; }
}
Modify ServerSide.cshtml
Apply the following file changes:
Add
@inject Serenity.ITextLocalizer Localizer
under@model IEnumerable<StartSharp.Northwind.Entities.CustomerRow>
Change
Title
withGetTitle(Localizer)
in<th>
lines like in the following:
<th>@fld.CustomerID.GetTitle(Localizer)</th>
...
Modify DataExplorerEndpoint.cs
Apply the following file changes:
Add the following code lines above ListResponse
method
protected ISqlConnections SqlConnections { get; }
public DataExplorerController(ISqlConnections sqlConnections)
{
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
}
Find the following line and
throw new ArgumentOutOfRangeException("serverType", (object)serverType, "Unknown server type");
with
throw new ArgumentOutOfRangeException(nameof(serverType), serverType, "Unknown server type");
Find the following line and
private string[] NumericTypes = new string[] { "Int32", "Int64", "Int16", "Decimal", "Double", "Single" };
with
private readonly string[] NumericTypes = new string[] { "Int32", "Int64", "Int16", "Decimal", "Double", "Single" };
Find the following line and
private static Dictionary<string, string> SqlTypeToFieldTypeMap =
with
private static readonly Dictionary<string, string> SqlTypeToFieldTypeMap =
Fix MeetingIndex.cshtml
Add
@inject Serenity.Web.IContentHashCache ContentHashCache
after@inject Serenity.ITextLocalizer Localizer
Replace
@ContentHashCache.ResolvePath("~/Scripts/ckeditor/")
with@ContentHashCache.ResolvePath(Context.Request.PathBase, "~/Scripts/ckeditor/")
Fix AccountLogin.AdminLTE.cshtml
Add
@inject IDataMigrations DataMigrations
after@inject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.Login.FormTitle;
withTexts.Forms.Membership.Login.FormTitle.ToString(Localizer);
Replace
@Texts.Forms.Membership.Login.RememberMe
with@Texts.Forms.Membership.Login.RememberMe.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.SignInButton
with@Texts.Forms.Membership.Login.SignInButton.ToString(Localizer)
Replace
@Texts.Forms.Membership.SignUp.ActivationCompleteMessage
with@Texts.Forms.Membership.SignUp.ActivationCompleteMessage.ToString(Localizer)
Replace
@Texts.Navigation.SiteTitle
with@Texts.Navigation.SiteTitle.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.FormTitle
with@Texts.Forms.Membership.Login.FormTitle.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.OR
with@Texts.Forms.Membership.Login.OR.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.FacebookButton
with@Texts.Forms.Membership.Login.FacebookButton.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.GoogleButton
with@Texts.Forms.Membership.Login.GoogleButton.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.ForgotPassword
with@Texts.Forms.Membership.Login.ForgotPassword.ToString(Localizer)
Replace
@Texts.Forms.Membership.Login.SignUpButton
with@Texts.Forms.Membership.Login.SignUpButton.ToString(Localizer)
Fix AccountLogin.cshtml
Add
@inject IDataMigrations DataMigrations
after@inject Serenity.ITextLocalizer Localizer
Replace
@Texts.Forms.Membership.SignUp.ActivationCompleteMessage
with@Texts.Forms.Membership.SignUp.ActivationCompleteMessage.ToString(Localizer)
Modify AccountPage.cs
Apply the following file changes in StartSharp:
or following file changes in Serene:
Modify BasicSamplesPage.Grids.cs
- Add
using Serenity.Data;
- Find
DragDropInTreeGrid()
method and its inside like in the following:
public ActionResult DragDropInTreeGrid([FromServices] ISqlConnections sqlConnections)
{
Repositories.DragDropSampleRepository.PopulateInitialItems(sqlConnections);
return View(Views.DragDropInTreeGrid.Index);
}
Modify DragDropSampleRepository.cs
- Add
using System;
- Find
PopulateInitialItems()
method and add the parameter insideISqlConnections sqlConnections
public static void PopulateInitialItems(ISqlConnections sqlConnections){ ... }
- Find this line:
using (var connection = SqlConnections.NewFor<MyRow>())
and change SqlConnections
with sqlConnections
(pay attention to uppercase and lowercase letters)
- Add these code lines above the
using (var connection = sqlConnections.NewFor<MyRow>())
if (sqlConnections is null)
throw new ArgumentNullException(nameof(sqlConnections));
at the end your method will be like:
public static void PopulateInitialItems(ISqlConnections sqlConnections)
{
if (sqlConnections is null)
throw new ArgumentNullException(nameof(sqlConnections));
using (var connection = sqlConnections.NewFor<MyRow>())
{
....
Fix AccountChangePassword.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.ChangePassword.FormTitle
withTexts.Forms.Membership.ChangePassword.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.ChangePassword.SubmitButton
withTexts.Forms.Membership.ChangePassword.SubmitButton.ToString(Localizer)
Fix AccountPage.ChangePassword.cs
Replace
public Result<ServiceResponse> ChangePassword(ChangePasswordRequest request)
withpublic Result<ServiceResponse> ChangePassword(ChangePasswordRequest request, [FromServices] IUserPasswordValidator passwordValidator)
Replace
Texts.Forms.Membership.ChangePassword.SubmitButton
withTexts.Forms.Membership.ChangePassword.SubmitButton.ToString(Localizer)
Add the following code lines inside the
ChangePassword(ChangePasswordRequest request, [FromServices] IUserPasswordValidator passwordValidator)
method
if (passwordValidator is null)
throw new ArgumentNullException(nameof(passwordValidator));
- Replace
Authorization.Username
withUser.Identity?.Name
Find the following line and
if (!Dependency.Resolve<IAuthenticationService>().Validate(ref username, request.OldPassword))
throw new ValidationError("CurrentPasswordMismatch", Texts.Validation.CurrentPasswordMismatch);
replace with
if (passwordValidator.Validate(ref username, request.OldPassword) != PasswordValidationResult.Valid)
throw new ValidationError("CurrentPasswordMismatch", Texts.Validation.CurrentPasswordMismatch.ToString(Localizer));
Replace
UserRepository.ValidatePassword(username, request.NewPassword, false)
withUserRepository.ValidatePassword(request.NewPassword, Localizer)
Replace
Authorization.UserId
withUser.GetIdentifier()
Fix AccountForgotPassword.AdminLTE.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.ForgotPassword.SubmitButton
withTexts.Forms.Membership.ForgotPassword.SubmitButton.ToString(Localizer)
Replace
Texts.Forms.Membership.ForgotPassword.FormInfo
withTexts.Forms.Membership.ForgotPassword.FormInfo.ToString(Localizer)
Replace
Texts.Forms.Membership.ForgotPassword.FormTitle
withTexts.Forms.Membership.ForgotPassword.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.ForgotPassword.BackToLogin
withTexts.Forms.Membership.ForgotPassword.BackToLogin.ToString(Localizer)
Fix AccountForgotPassword.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.ForgotPassword.FormTitle
withTexts.Forms.Membership.ForgotPassword.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.ForgotPassword.FormInfo
withTexts.Forms.Membership.ForgotPassword.FormInfo.ToString(Localizer)
Replace
Texts.Forms.Membership.ForgotPassword.SubmitButton
withTexts.Forms.Membership.ForgotPassword.SubmitButton.ToString(Localizer)
Fix AccountPage.ForgotPassword.cs
Add
using Microsoft.Extensions.Options;
andusing StartSharp.Common;
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(public\s*Result<ServiceResponse>\s*ForgotPassword\s*\(\s*ForgotPasswordRequest\s*([A-Za-z0-9_]*))\)(\r\n\s*)
inFind
inputType
$1,$3[FromServices] IEmailSender emailSender,$3[FromServices] IOptions<EnvironmentSettings> options = null)$3
inReplace
inputClick
Replace All
Type
(.*Texts.Validation.CantFindUserWithEmail)(\s*)
inFind
inputType
$1.ToString(Localizer)$2
inReplace
inputClick
Replace All
Type
Config.Get<EnvironmentSettings>\s*\(\).SiteExternalUrl
inFind
inputType
options?.Value.SiteExternalUrl
inReplace
inputClick
Replace All
Type
(.*Texts.Forms.Membership.ResetPassword.EmailSubject).ToString\(\)(\s*)
inFind
inputType
$1.ToString(Localizer)$2
inReplace
inputClick
Replace All
Type
(.*)Common.EmailHelper.Send\(([a-zA-Z0-9_]*)\s*,\s*([a-zA-Z0-9_]*)\s*,\s*(user.Email)\s*\)\s*;(\r\n\s*)
inFind
inputType
$1if (emailSender is null)\n$1\tthrow new ArgumentNullException(nameof(emailSender));$5emailSender.Send(subject: $2, body: $3, mailTo: $4);$5
inReplace
inputClick
Replace All
Fix ProductExcelImportEndpoint.cs
- Remove
using System.IO;
- Find
ExcelImport()
method and add 3rd parameter[FromServices] IUploadStorage uploadStorage
. Your method should be like:
public ExcelImportResponse ExcelImport(IUnitOfWork uow, ExcelImportRequest request,
[FromServices] IUploadStorage uploadStorage)
{
...
- Find exceptions contains
"filename"
parameters
ArgumentNullException("filename");
ArgumentOutOfRangeException("filename");
and change with nameof(request.FileName)
like in the following:
ArgumentNullException(nameof(request.FileName));
ArgumentOutOfRangeException(nameof(request.FileName));
- Find this line:
using (var fs = new FileStream(UploadHelper.DbFilePath(request.FileName), FileMode.Open, FileAccess.Read))
and change with:
using (var fs = uploadStorage.OpenFile(request.FileName))
- (optional) You can change
ProductRow
withMyRow
except using lineusing MyRow = StartSharp.Northwind.Entities.ProductRow;
Fix AccountPage.ResetPassword.cs
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*public\s*ActionResult\s*ResetPassword\s*\(\s*string\s*[a-zA-Z0-9_]*\s*)(\))
inFind
inputType
$1, [FromServices] ISqlConnections sqlConnections$2
inReplace
inputClick
Replace All
Type
(.*Texts.Validation.InvalidResetToken)(\s*)
inFind
inputType
$1.ToString(Localizer)$2
inReplace
inputClick
Replace All
Type
((.*)using\s*\(\s*var\s*([a-zA-Z0-9]*)\s*=\s*)SqlConnections(.NewFor<UserRow>\(\)\))
inFind
inputType
$2if (sqlConnections is null)\n$2\tthrow new ArgumentNullException(nameof(sqlConnections));\n\n$1sqlConnections$4
inReplace
inputClick
Replace All
Type
(.*public\s*Result<ServiceResponse>\s*ResetPassword\s*\(\s*ResetPasswordRequest\s*[a-zA-Z0-9_]*\s*)(\))
inFind
inputType
$1, [FromServices] ISqlConnections sqlConnections$2
inReplace
inputClick
Replace All
Type
(UserRepository.ValidatePassword\s*\()\s*[a-zA-Z0-9_.]*\s*,\s*(request.NewPassword)\s*,\s*false(.*)
inFind
inputType
$1$2, Localizer$3
inReplace
inputClick
Replace All
Fix AccountResetPassword.AdminLTE.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.ResetPassword.FormTitle
withTexts.Forms.Membership.ResetPassword.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.ResetPassword.SubmitButton
withTexts.Forms.Membership.ResetPassword.SubmitButton.ToString(Localizer)
Replace
@Texts.Navigation.SiteTitle
with@Texts.Navigation.SiteTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.ResetPassword.BackToLogin
withTexts.Forms.Membership.ResetPassword.BackToLogin.ToString(Localizer)
Fix AccountResetPassword.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.ResetPassword.FormTitle
withTexts.Forms.Membership.ResetPassword.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.ResetPassword.SubmitButton
withTexts.Forms.Membership.ResetPassword.SubmitButton.ToString(Localizer)
Modify CustomerGrossSalesEndpoint.cs
Add
using Serenity.PropertyGrid;
Find
DynamicDataReport()
method and add 3rd parameterHttpContext.RequestServices
. Your method should be like:
var report = new DynamicDataReport(data, request.IncludeColumns,
typeof(Columns.CustomerGrossSalesColumns), HttpContext.RequestServices);
- Find this line
var bytes = new ReportRepository(Context).Render(report);
and change with
var bytes = ReportRepository.Render(report);
Modify BackgroundJobManager.cs
- Find this line
isDisabled = !Config.Get<Settings>().Enabled;
and change with
isDisabled = !options.Value.Enabled;
this.logger = logger;
Modify DailyBackgroundJob.cs
- Find
Initialize()
method and copy and paste in the following lines above that method
protected ILogger Log { get; }
protected IExceptionLogger ExceptionLog { get; }
protected DailyBackgroundJob(ILogger log, IExceptionLogger exceptionLog)
{
Log = log;
ExceptionLog = exceptionLog;
}
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(using\s*Serenity;)
inFind
inputType
$1\nusing Microsoft.Extensions.Logging;\nusing Serenity.Abstractions;
inReplace
inputClick
Replace All
Type
((.*)(private\s*.*retryCount;)())
inFind
inputType
$1\n\n$2protected ILogger Log { get; }\n$2protected IExceptionLogger ExceptionLog { get; }\n\n$2protected DailyBackgroundJob(ILogger log, IExceptionLogger exceptionLog)\n$2{\n$2\tLog = log;\n$2\tExceptionLog = exceptionLog;\n$2}
inReplace
inputClick
Replace All
Type
(.*)this.(GetType.*)
inFind
inputType
$1$2
inReplace
inputClick
Replace All
Type
((.*)(Log)(.*.*\(\))(\);))
inFind
inputType
$2$3?.LogInformation(job$5
inReplace
inputClick
Replace All
Type
((.*)(.Log\()(\);))
inFind
inputType
$2$3ExceptionLog$4
inReplace
inputClick
Replace All
Fix AccountPage.SignUp.cs
Add
using StartSharp.Common;
Add
using Microsoft.Extensions.Options;
Remove
using MailKit.Net.Smtp;
Remove
using using MimeKit;
Remove
using MailKit.Security;
Remove
usinusing System.Web.Hosting;
Replace
throw new ArgumentNullException(""email"");
withthrow new ArgumentNullException(nameof(request.Email));
Replace
throw new ArgumentNullException(""password"");
withthrow new ArgumentNullException(nameof(request.Password));
Replace
throw new ArgumentNullException(""displayName"");
withthrow new ArgumentNullException(nameof(request.DisplayName));
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(public\s*Result<ServiceResponse>\s*SignUp\s*\(\s*SignUpRequest\s*([A-Za-z0-9_]*))\)(\r\n\s*)
inFind
inputType
$1,$3\t[FromServices] IEmailSender emailSender,$3\t[FromServices] IOptions<EnvironmentSettings> options = null)$3
inReplace
inputClick
Replace All
Type
(.*Texts.Validation.EmailInUse)(\s*)
inFind
inputType
$1.ToString(Localizer)$2
inReplace
inputClick
Replace All
Type
Config.Get<EnvironmentSettings>\s*\(\).SiteExternalUrl
inFind
inputType
options?.Value.SiteExternalUrl
inReplace
inputClick
Replace All
Type
(.*)Common.EmailHelper.Send\(([a-zA-Z0-9_]*)\s*,\s*([a-zA-Z0-9_]*)\s*,\s*([a-zA-Z0-9_]*)\s*\)\s*;(\r\n\s*)
inFind
inputType
$1if (emailSender is null)\n$1\tthrow new ArgumentNullException(nameof(emailSender));$5emailSender.Send(subject: $2, body: $3, mailTo: $4);$5
inReplace
inputClick
Replace All
Type
(.*UserRetrieveService.RemoveCachedUser\s*\()\s*([a-zA-Z0-9_]*)\s*,\s*([a-zA-Z0-9_]*)(\s*\);)
inFind
inputType
$1Cache, $2, $3$4
inReplace
inputClick
Replace All
Type
(.*Texts.Validation.InvalidActivateToken)(\s*)
inFind
inputType
$1.ToString(Localizer)$2
inReplace
inputClick
Replace All
Fix AccountSignUp.AdminLTE.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.SignUp.FormTitle
withTexts.Forms.Membership.SignUp.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.SignUp.SubmitButton
withTexts.Forms.Membership.SignUp.SubmitButton.ToString(Localizer)
Replace
Texts.Forms.Membership.SignUp.AcceptTerms
withTexts.Forms.Membership.SignUp.AcceptTerms.ToString(Localizer)
Replace
Texts.Navigation.SiteTitle
withTexts.Navigation.SiteTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.SignUp.FormInfo
withTexts.Forms.Membership.SignUp.FormInfo.ToString(Localizer)
Replace
Texts.Forms.Membership.SignUp.BackToLogin
withTexts.Forms.Membership.SignUp.BackToLogin.ToString(Localizer)
Fix AccountSignUp.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Forms.Membership.SignUp.FormTitle
withTexts.Forms.Membership.SignUp.FormTitle.ToString(Localizer)
Replace
Texts.Forms.Membership.SignUp.FormInfo
withTexts.Forms.Membership.SignUp.FormInfo.ToString(Localizer)
Replace
Texts.Forms.Membership.SignUp.SubmitButton
withTexts.Forms.Membership.SignUp.SubmitButton.ToString(Localizer)
Modify CategoryRepository.cs
- Find the following method and remove
protected override void AfterSave()
{
base.AfterSave();
if (Request.Localizations != null)
foreach (var pair in Request.Localizations)
{
pair.Value.CategoryID = Row.CategoryID.Value;
new LocalizationRowHandler<MyRow>().Update<Entities.CategoryLangRow>(this.UnitOfWork, pair.Value, Convert.ToInt32(pair.Key));
}
}
- Add following method to inside the
private class MySaveHandler..
code block
public MySaveHandler(IRequestContext context)
: base(context)
{
}
Fix CategoryRow.cs
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(\[DisplayName\(""Category Name""\).*NameProperty)]
inFind
inputType
$1, Localizable(true)]
inReplace
inputClick
Replace All
Type
(\[DisplayName\(""Description""\).*QuickSearch)]
inFind
inputType
$1, Localizable(true)]
inReplace
inputClick
Replace All
Fix CustomerEndpoint.cs
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
((.*)var\s*[a-zA-Z0-9_]*\s*=\s*new\s*DynamicDataReport\s*\(([a-zA-Z0-9_]*)\s*,\s*([a-zA-Z0-9_.]*)\s*,\s*typeof\(([a-zA-Z0-9_.]*)\s*\))\);
inFind
inputType
$1,\n$2\tHttpContext.RequestServices);
inReplace
inputClick
Replace All
Type
(.*var\s*[a-zA-Z0-9_]*\s*=\s*)new\s*(ReportRepository)\(Context\)(.Render\(\s*([a-zA-Z0-9_]*)\);)
inFind
inputType
$1$2$3
inReplace
inputClick
Replace All
Fix CustomerIndex.cshtml
Add
@inject IContentHashCache ContentHashCache
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*@ContentHashCache.ResolvePath\(\s*)(\"".*)
inFind
inputType
$1Context.Request.PathBase, $2
inReplace
inputClick
Replace All
Fix CustomerLookup.cs
- Add
using Serenity.Data;
Fix CustomerRepository.cs
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*SqlExceptionHelper.HandleDeleteForeignKeyException\(\s*[a-zA-Z0-9_]*\s*)(\);)
inFind
inputType
$1, Localizer$2
inReplace
inputClick
Replace All
Fix CustomerRow.cs
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*\[\s*)(LookupEditor\(\s*typeof\(\s*EmployeeRow\s*\)\s*,\s*Multiple\s*=\s*true\s*\)\s*,\s*NotMapped\])
inFind
inputType
$1DisplayName(""Representatives""), $2
inReplace
inputClick
Replace All
Fix EmployeeListDecorator.cs
Add
using Serenity.Abstractions;
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*public\s*class\s*EmployeeListDecorator\s*:\s*BaseCellDecorator\r\n\s*{(\r\n\s*))
inFind
inputType
$1public EmployeeListDecorator(ITwoLevelCache cache, ISqlConnections sqlConnections)$2{$2\tCache = cache ?? throw new ArgumentNullException(nameof(cache));$2\tSqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));$2}\n$2public ITwoLevelCache Cache { get; }$2public ISqlConnections SqlConnections { get; }\n$2
inReplace
inputClick
Replace All
Modify PeriodicBackgroundJob.cs
- Find
Initialize()
method and copy and paste in the following lines above that method
protected ILogger Log { get; }
protected IExceptionLogger ExceptionLog { get; }
protected DailyBackgroundJob(ILogger log, IExceptionLogger exceptionLog)
{
Log = log;
ExceptionLog = exceptionLog;
}
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(using\s*Serenity;)
inFind
inputType
$1\nusing Microsoft.Extensions.Logging;\nusing Serenity.Abstractions;
inReplace
inputClick
Replace All
Type
((.*)(protected\s*.*InternalRun\(\);)())
inFind
inputType
$1\n\n$2protected ILogger Log { get; }\n$2protected IExceptionLogger ExceptionLog { get; }\n\n$2protected DailyBackgroundJob(ILogger log, IExceptionLogger exceptionLog)\n$2{\n$2\tLog = log;\n$2\tExceptionLog = exceptionLog;\n$2}
inputClick
Replace All
Type
(.*Log).Info(\s*\([a-zA-Z0-9_:\s""]*\+\s*)this.(GetType\(\).Name\s*\+\s*[a-zA-Z0-9_\s""]*\+\s*\w*\s*),\s*this.GetType\(\)\);
inFind
inputType
$1?.LogInformation$2$3);
inputClick
Replace All
Type
(.*Log).Info(\s*\([a-zA-Z0-9_:\s""]*\+\s*)this.(GetType\(\).Name\s*\+\s*[a-zA-Z0-9_\s""]*.*."")(,\s*this.GetType\(\)\);)
inFind
inputType
$1?.LogInformation$2$3);
inputClick
Replace All
Fix NotesBehavior.cs
Open
Replace
dialog in Visual StudioCtrl+H
Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*public\s*class\s*NotesBehavior\s*:\s*.*\r\n\s*{(\r\n\s*))
inFind
inputType
$1public IRequestContext Context { get; }$2public ISqlConnections SqlConnections { get; }\n$2public NotesBehavior(IRequestContext context, ISqlConnections sqlConnections)$2{$2\tContext = context ?? throw new ArgumentNullException(nameof(context));$2\tSqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));$2}\n$2
inReplace
inputClick
Replace All
Type
(.*idField)\[\s*(handler.Row)\s*\]\s+
inFind
inputType
$1.AsObject($2)
inReplace
inputClick
Replace All
Type
(.*)(rowIdField)\[\s*([a-zA-Z0-9_]*)\s*\].Value
inFind
inputType
$1Convert.ToInt64($2.AsObject($3))
inReplace
inputClick
Replace All
Type
(.*)(rowIdField)\[\s*([a-zA-Z0-9_]*)\s*\];
inFind
inputType
$1$2.AsObject($3);
inReplace
inputClick
Replace All
Type
(.*)(idField)\[\s*([a-zA-Z0-9_.]*)\s*\].Value(.*)
inFind
inputType
$1Convert.ToInt64($2.AsObject($3))$4
inReplace
inputClick
Replace All
Type
(.*)(id).Value
inFind
inputType
$1Convert.ToInt64($2)
inReplace
inputClick
Replace All
Type
(.*Log\?.LogInformation)(\s*\([a-zA-Z0-9_:\s""]*\+\s*)(GetType\(\).Name)(\s*\+\s*""."")(\));
inFind
inputType
$1$2$3);
inputClick
Replace All
Type
((.*)(.Log\()(\);))
inFind
inputType
$2$3ExceptionLog$4
inputClick
Replace All
Fix DashboardIndex.cshtml
- Replace
Texts.Site.Dashboard.ContentDescription
withTexts.Site.Dashboard.ContentDescription.ToString(Localizer)
Fix DashboardPage.cs
Add
using Serenity.Abstractions;
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
((.*)public\s*ActionResult\s*Index\()(\).*)
inFind
inputType
$1\n$2\t//<if:Northwind>\n$2\t[FromServices] ITwoLevelCache cache,\n$2\t[FromServices] ISqlConnections sqlConnections\n$2\t//</if:Northwind>\n$2\t)
inReplace
inputClick
Replace All
Type
(((.*)var\s*[a-zA-Z0-9_]*\s*=\s*)Cache(.GetLocalStoreOnly\s*\([a-zA-Z0-9_""]*\s*,\s*TimeSpan.FromMinutes\(5\),))
inFind
inputType
$3if (cache is null)\n$3\tthrow new ArgumentNullException(nameof(cache));\n\n$3if (sqlConnections is null)\n$3\tthrow new ArgumentNullException(nameof(sqlConnections));\n\n$2cache$4
inReplace
inputClick
Replace All
Type
(.*)SqlConnections(.NewFor.*)
inFind
inputType
$1sqlConnections$2
inReplace
inputClick
Replace All
Fix EmailIndex.cshtml
Add
@injeinject Serenity.ITextLocalizer Localizer
Replace
Texts.Site.EmailClient.PageTitle
withTexts.Site.EmailClient.PageTitle.ToString(Localizer)
Fix EmailPage.cs
Add
using Microsoft.Extensions.Caching.Memory;
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*public\s*ActionResult\s*Index\()(\).*)
inFind
inputType
$1[FromServices] IMemoryCache memoryCache$2
inReplace
inputClick
Replace All
Type
(.*)LocalCache(.*)Authorization.UserId(.*)
inFind
inputType
$1memoryCache?$2User.GetIdentifier()$3
inReplace
inputClick
Replace All
Type
(.*)(var.*.GetAttachmentList.*)(\);)
inFind
inputType
$1$2,\n$1\tRequest.PathBase$3
inReplace
inputClick
Replace All
Fix SqlExceptionHelper.cs
Add
using Microsoft.Data.SqlClient;
Add
using Serenity;
Remove
using System.Data.SqlClient;
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*HandleDeleteForeignKeyException\s*\(\s*Exception e)(\))
inFind
inputType
$1, ITextLocalizer localizer$2
inReplace
inputClick
Replace All
Type
(.*ValidationError.*DeleteForeignKeyError)(,.*)
inFind
inputType
$1.ToString(localizer)$2
inReplace
inputClick
Replace All
Type
(.*HandleSavePrimaryKeyException\s*\(\s*Exception e)\s*,(\s*string\s*[a-zA-Z0-9_]*.*)
inFind
inputType
$1, ITextLocalizer localizer,$2
inReplace
inputClick
Replace All
Type
(.*ValidationError.*SavePrimaryKeyError)(,.*)
inFind
inputType
$1.ToString(localizer)$2
inReplace
inputClick
Replace All
Fix MailingBackgroundJob.cs
Add
using Microsoft.Extensions.Logging;
Remove
private readonly IExceptionLogger logger;
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*)(public\s*MailingBackgroundJob\s*\(\s*ISqlConnections\s*[a-zA-Z0-9_]*,)\s*(IEmailSender\s*[a-zA-Z0-9_]*),\s*(IOptions<MailingServiceSettings>\s*[a-zA-Z0-9_]*),\s*IExceptionLogger.*
inFind
inputType
$1$2\n$1\t$3, $4,\n$1\tILogger<MailingBackgroundJob> log = null, IExceptionLogger exceptionLog = null)\n$1\t: base(log, exceptionLog)
inReplace
inputClick
Replace All
Remove
this.logger = logger;
Replace
ex.Log(logger)
withex.Log(ExceptionLog)
Modify DynamicDataReport.cs
Add
using Microsoft.Extensions.DependencyInjection;
Add
using Serenity.Abstractions;
Add
using Serenity.ComponentModel;
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
((.*)(protected\s*Type\s*.*{\s*)get;\s*private\s*.*})
inFind
inputType
$1\n$2protected IServiceProvider serviceProvider;
inputClick
Replace All
Find this method
DynamicDataReport()
line
public DynamicDataReport(IEnumerable data, IEnumerable<string> columnList, Type columnsType)
and add this line above method DynamicDataReport()
const string CacheGroupKey = "DynamicDataReportColumns";
- Add a parameter as (4th parameter)
IServiceProvider serviceProvider
into the same methodDynamicDataReport()
and replace inside of that method line in the following:
public DynamicDataReport(IEnumerable data, IEnumerable<string> columnList, Type columnsType, IServiceProvider serviceProvider)
{
Data = data ?? throw new ArgumentNullException(nameof(data));
ColumnList = columnList ?? new List<string>();
ColumnsType = columnsType;
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
Type
(.*)(var\s*[a-zA-Z|]*\s=\s*new\s*List.*;)
inFind
inputType
$1return GetColumnListFor(ColumnsType, ColumnList, serviceProvider);\n\t\t}\n\n\t\tpublic static List<ReportColumn> GetColumnListFor(Type columnsType,\n$1IEnumerable<string> columnOrder, IServiceProvider serviceProvider)\n\t\t{\n$1if (columnsType == null)\n$1\tthrow new ArgumentNullException(nameof(columnsType));
inputClick
Replace All
Type
(.*)(if\s*\(!ColumnList.Any\(*.*\))
inFind
inputType
$1var list = new List<ReportColumn>();\n$1if (columnOrder != null && !columnOrder.Any())
inputClick
Replace All
Type
(.*)(IDictionary<string,\s*)(.*Items\s*=\s*null;)
inFind
inputType
$1List<$3\n$1IDictionary<string, PropertyItem> propertyItemByName = null;
inputClick
Replace All
Find this line
IRow basedOnRow = null;
and under that line find the if block
if (columnsType != null)
and customize that if block like in the following (customize your codes if variables or etc. need some changes):
if (columnsType != null)
{
var cache = serviceProvider.GetRequiredService<ITwoLevelCache>();
propertyItems = cache.GetLocalStoreOnly("DynamicDataReport:Columns:" + columnsType.FullName,
TimeSpan.Zero, CacheGroupKey, () =>
{
var propertyItemProvider = serviceProvider.GetRequiredService<IPropertyItemProvider>();
var items = propertyItemProvider.GetPropertyItemsFor(columnsType).ToList();
if (typeof(ICustomizedFormScript).IsAssignableFrom(columnsType))
{
var instance = ActivatorUtilities.CreateInstance(serviceProvider, columnsType) as ICustomizedFormScript;
instance.Customize(items);
}
return items;
});
propertyItemByName = propertyItems.ToDictionary(x => x.Name);
propertyInfos = columnsType.GetProperties().ToDictionary(x => x.Name);
var basedOnAttr = columnsType.GetCustomAttribute<BasedOnRowAttribute>();
if (basedOnAttr != null &&
basedOnAttr.RowType != null &&
!basedOnAttr.RowType.IsInterface &&
!basedOnAttr.RowType.IsAbstract &&
typeof(IRow).IsAssignableFrom(basedOnAttr.RowType))
{
basedOnRow = (IRow)Activator.CreateInstance(basedOnAttr.RowType);
}
}
Type
(.*)(foreach\s*\(var\s*columnName\s*in\s*ColumnList\))
inFind
inputType
$1if (columnOrder == null)\n$1\tcolumnOrder = propertyItems.Select(x => x.Name).ToList();\n\n$1foreach (var columnName in columnOrder)
inReplace
inputClick
Replace All
Type
(.*)(PropertyItem\s*item;\r\n\s*.*.*\))
inFind
inputType
$1if (!propertyItemByName.TryGetValue(columnName, out PropertyItem item))
inReplace
inputClick
Replace All
Type
(.*)(var\s*basedOnField\s*=\s*.*\?\s*)(\(Field\))(null\s*:)
inFind
inputType
$1$2$4
inReplace
inputClick
Replace All
Type
(.*)(PropertyInfo\s*p;)\r\n\s*.*\)
inFind
inputType
$1if (propertyInfos == null || !propertyInfos.TryGetValue(columnName, out PropertyInfo p))
inReplace
inputClick
Replace All
Type
(.*)(list.Add\(.*.*\sp)(\)\);)
inFind
inputType
$1$2, serviceProvider, serviceProvider.GetRequiredService<ITextLocalizer>()$3
inReplace
inputClick
Replace All
Type
(.*)(private\s*ReportColumn\s*.*\))
inFind
inputType
$1public static ReportColumn FromPropertyItem(PropertyItem item, Field field, PropertyInfo property,\n$1\tIServiceProvider provider, ITextLocalizer localizer)\n$1{\n$1\tif (item is null)\n$1\t\tthrow new ArgumentNullException(nameof(item));\n\n$1\tif (localizer is null)\n$1\t\tthrow new ArgumentNullException(nameof(localizer));\n\n$1\tvar result = new ReportColumn
inReplace
inputClick
Replace All
Type
(.*)(var\s*result\s*=\s*new\s*ReportColumn)(\r\n\s*{)(\r\n\s*var.*\r\n\s*.*\r\n\s*.*.*;)
inFind
inputType
$1$2\n$1{\n$1\tName = item.Name,\n$1\tTitle = item.Title ?? item.Name\n$1};\n
inReplace
inputClick
Replace All
Type
(.*)(result.Title\s*=\s*)(L)(.*.*;)
inFind
inputType
$1$2l$4
inReplace
inputClick
Replace All
Find this if block and
if (!string.IsNullOrWhiteSpace(item.DisplayFormat))
{
result.Format = item.DisplayFormat;
}
replace like in the following lines:
if (!string.IsNullOrWhiteSpace(item.DisplayFormat))
{
if (item.FormatterType == "Date" || item.FormatterType == "DateTime")
{
result.Format = item.DisplayFormat switch
{
"d" => DateHelper.CurrentDateFormat,
"g" => DateHelper.CurrentDateTimeFormat.Replace(":ss", ""),
"G" => DateHelper.CurrentDateTimeFormat,
"s" => "yyyy-MM-ddTHH:mm:ss",
"u" => "yyyy-MM-ddTHH:mm:ss.fffZ",
_ => item.DisplayFormat,
};
}
else
result.Format = item.DisplayFormat;
}
Type
(!ReferenceEquals\(null,\s*[a-zA-Z|]*\)\s*&&)
inFind
inputType
dtf is object &&
inReplace
inputClick
Replace All
Replace
else if (!ReferenceEquals(null, dtf))
withelse if (dtf is object)
Replace
if (!ReferenceEquals(null, field))
withif (field is object)
Replace
result.DataType = !ReferenceEquals(null, field) ? field.ValueType : null;
withresult.DataType = field?.ValueType;
Type
(.*)(result.Format\s=\s*)("")(.*.*;)
inFind
inputType
$1$2DateHelper.CurrentDateTimeFormat;
inReplace
inputClick
Replace All
Type
(.*)(var\s*[a-zA-Z_]*\s*=\s*[a-zA-Z_]*\s*as\s*.*.\r\n\s*.*[a-zA-Z_]*.*.*[a-zA-Z_]*.*EnumType.*\))
inFind
inputType
$1if (field is IEnumTypeField enumField && enumField.EnumType != null)
inReplace
inputType
(.*)(result.Decorator\s*=\s*new\s*EnumDecorator\([a-zA-Z_]*.EnumType)(\);)
inFind
inputType
$1$2, localizer$3
inReplace
inputClick
Replace All
Type
(.*)([a-zA-Z_]*result.Title\s*=\s*[a-zA-Z_]*.)(Title)(;)
inFind
inputType
$1$2GetTitle(localizer)$4
inReplace
inputClick
Replace All
Modify ExcelReportGenerator.cs
- Remove
using Serenity.Reflection;
- Remove
using System.Runtime.InteropServices;
- Replace
var worksheet = package.Workbook.Worksheets.Add(sheetName);
withvar worksheet = workbook.Worksheets.Add(sheetName);
- Replace
var row = obj as Row;
withvar row = obj as IRow;
- Replace
!Object.Equals(decorator.Value, value)
with!Equals(decorator.Value, value)
Modify UserPreferenceRepository.cs
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*)(var\s*[a-zA-Z_]*\s*=\s*)(\(.*.*)(\;)
inFind
inputType
$1$2Convert.ToInt32(Context.User.GetIdentifier())$4
inReplace
inputClick
Replace All
Fix NavigationModel.cs
Add
using Microsoft.Extensions.DependencyInjection;
Add
using Serenity.Abstractions;
Add
using Serenity.Web;
Add
using StartSharp.Administration.Entities;
Remove
using System.Security.Claims;
Remove
using System.Web;
Remove
using System.Web.Hosting;
Remove
var requestUrl = httpContext.Request.GetDisplayUrl();
Add the following method
public NavigationModel(HttpContext httpContext)
: this(
httpContext?.RequestServices?.GetRequiredService<ITwoLevelCache>(),
httpContext?.RequestServices?.GetRequiredService<IPermissionService>(),
httpContext?.RequestServices?.GetRequiredService<ITypeSource>(),
httpContext?.RequestServices,
httpContext?.User,
httpContext?.Request?.Path + httpContext?.Request?.QueryString,
httpContext?.Request?.PathBase ?? "")
{
}
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*public\s*)partial\s*(class\s*NavigationModel\r\n\s*{\r\n(\s*))
inFind
inputType
$1$2public IPermissionService Permissions { get; }\n$3public HttpContext HttpContext { get; }\n$3public string RequestUrl { get; }\n$3public PathString PathBase { get; }\n$3
inReplace
inputClick
Replace All
Type
(.*)(public\s*NavigationModel\()(\))(\r\n\s*{)(\r\n\s*)
inFind
inputType
$1$2ITwoLevelCache cache, IPermissionService permissions,$5ITypeSource typeSource, IServiceProvider services, ClaimsPrincipal user,$5string requestUrl, PathString pathBase)$4$5if (cache is null)$5\tthrow new ArgumentNullException(nameof(cache));$5$5
inReplace
inputClick
Replace All
Type
(.*)(Items\s*=\s*)Cache(.*)\(Authorization.UserId\s*\?\?\s*""-1""\)(,\s*TimeSpan.Zero,)
inFind
inputType
$1$2cache$3\n\t$1\t(user?.GetIdentifier() ?? "-1")$4
inReplace
inputClick
Replace All
Type
(.*)(NavigationHelper.GetNavigationItems\s*\()\s*(x\s*=>\s*)\r\n\s*(x\s*\!=\s*.*StartsWith.*\?)\s*(VirtualPathUtility.ToAbsolute.*)
inFind
inputType
$1$2permissions, typeSource,\n$1\tservices, $3$4\n$1\t\t$5
inReplace
inputClick
Replace All
Type
(.*)(SetActivePath\s*\(\s*\);)
inFind
inputType
$1RequestUrl = requestUrl;\n$1PathBase = pathBase;\n$1$2
inReplace
inputClick
Replace All
Type
(.*)var\s*.*.HttpContext;\r\n\s*if\s*\(\s*httpContext\s*\!=\s*null\s*\)
inFind
inputType
$1if (RequestUrl != null)
inReplace
inputClick
Replace All
Type
(.*currentUrl\s*=\s*)requestUrl.ToString\(\);
inFind
inputType
$1RequestUrl;
inReplace
inputClick
Replace All
Type
(.*)(if\s*\(\s*\!)requestUrl.ToString\(\)(.EndsWith\(""/""\)\s*&&\r\n\s*)String(.Compare\()httpContext.Request.Path,\s*\r\n\s*.*.ApplicationVirtualPath(,.*OrdinalIgnoreCase\s*\)\s*==\s*0\))
inFind
inputType
$1$2currentUrl$3string$4currentUrl.Split('?')[0], PathBase$5
inReplace
inputClick
Replace All
Type
(.*VirtualPathUtility.ToAbsolute\s*\()([a-zA-Z0-9_]*\))
inFind
inputType
$1PathBase, $2
inReplace
inputClick
Replace All
Modify ReportTree.cs
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
(.*public\s*static\s*ReportTree.*Report\>\s*reports,\s*)(\r\n\s*)
inFind
inputType
$1ITextLocalizer localizer,$2
inputClick
Replace All
Type
(.*.GetReportCategoryTitle\s*\()([a-zA-Z0-9_]*\s*)(\);)
inFind
inputType
$1$2, localizer$3
inputClick
Replace All
Modify UserDataScript.cs
If you have a project created from a recent StartSharp template than make some changes in the following:
Fix FilePage.cs
Add
using Microsoft.AspNetCore.Http;
Remove
using System.Web;
Remove
using HttpContextBase = Microsoft.AspNetCore.Http.HttpContext;
Add the following code block
public FileController(IUploadStorage uploadStorage, ITextLocalizer localizer)
{
UploadStorage = uploadStorage ??
throw new ArgumentNullException(nameof(uploadStorage));
Localizer = localizer ??
throw new ArgumentNullException(nameof(localizer));
}
protected IUploadStorage UploadStorage { get; }
protected ITextLocalizer Localizer { get; }
Replace
this.HttpContext
withHttpContext
Open
Replace
dialog in Visual StudioCtrl+H
(be sure thatCurrent Document
is selected)Make sure
Match case
is Checked,Match whole word
is NOT checked andUse regular expressions
is Checked.Type
UploadHelper(.CheckFileNameSecurity.*)
inFind
inputType
UploadPathHelper$1
inputClick
Replace All
Type
(.*)var\s*[a-zA-Z0-9_]*\s*=\s*UploadHelper.DbFilePath\((.*)\);(\r\n\s*)(var\s*[a-zA-Z0-9_]*\s*=\s*KnownMimeTypes.Get\().*(\);)\r\n\s*return\s*new\s*PhysicalFileResult.*
inFind
inputType
$1if (!UploadStorage.FileExists($2))$3\treturn new NotFoundResult();\n$3$4$2$5$3var stream = UploadStorage.OpenFile($2);$3return new FileStreamResult(stream, mimeType);
inputClick
Replace All
Type
(.*VirtualPathUtility.ToAbsolute).*(\r\n\s*).*UploadHelper.ImageFileUrl(.*TemporaryFile).*
inFind
inputType
$1(HttpContext,$2UploadStorage.GetFileUrl$3))
inputClick
Replace All
Type
(.*HandleUploadRequest\s*\(\s*)HttpContextBase
inFind
inputType
$1HttpContext
inputClick
Replace All
Type
(.*new\s*UploadProcessor)(\r\n\s*{)
inFind
inputType
$1(UploadStorage)$2
inputClick
Replace All
Type
(.*)(if\s*\(.*ProcessStream.*OpenReadStream.*)(Path.GetExtension\s*\(\s*.*FileName\)).*
inFind
inputType
$1$2\n$1\t$3, Localizer))
inputClick
Replace All
Type
(.*)var\s*([a-zA-Z0-9_]*)\s*=.*Path.GetFileName.*(\r\n\s*)using.*StreamWriter.*OpenWrite.*DbFilePath.*\r\n\s*.*WriteLine.*FileName.*
inFind
inputType
$1var $2 = processor.TemporaryFile;$3UploadStorage.SetOriginalName($2, file.FileName);
inputClick
Replace All
Changes About SqlConnections Static Class for Dependency Injection
For example OrderDetailReport.cs
uses SqlConnection
static class from .Net3.1
. In .Net5
we changed SqlConnection
to instance class and it's can be injected via DI
. For OrderDetailReport.cs
we need to add a constructor what has property of ISqlConnections
then if we assign this to SqlConnection
property what type of ISqlConnection
, then we didn't need any change in rest of document. You need to do this on all of your files what uses SqlConnections
static class.
From
[RequiredPermission(PermissionKeys.General)]
public class OrderDetailReport : IReport, ICustomizeHtmlToPdf
{
public Int32 OrderID { get; set; }
public object GetData()
{
var data = new OrderDetailReportData();
//Gives error for SqlConnections
using (var connection = SqlConnections.NewFor<OrderRow>())
{
var o = OrderRow.Fields;
To
[RequiredPermission(PermissionKeys.General)]
public class OrderDetailReport : IReport, ICustomizeHtmlToPdf
{
public OrderDetailReport(ISqlConnections sqlConnections)
{
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
}
protected ISqlConnections SqlConnections { get; }
public int OrderID { get; set; }
public object GetData()
{
var data = new OrderDetailReportData();
//Works fine
using (var connection = SqlConnections.NewFor<OrderRow>())
{
var o = OrderRow.Fields;
Changes in ReportsPage
Reports page has a few changes too. You need to add constructor with dependencies
Constructor
public IReportRegistry ReportRegistry { get; }
protected ITextLocalizer Localizer { get; }
public ReportRepository(IRequestContext context, IReportRegistry reportRegistry, ITextLocalizer localizer)
: base(context)
{
ReportRegistry = reportRegistry ??
throw new ArgumentNullException(nameof(reportRegistry));
Localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
}
Render method needs to be static
From
public byte[] Render(IDataOnlyReport report)
{
...
}
To
public static byte[] Render(IDataOnlyReport report)
{
...
}
ReportTree.FromList
needs localizer
as new parameter. Find this lines and add localizer
parameter on it.
From
return ReportTree.FromList(reports, category);
To
return ReportTree.FromList(reports, Localizer, category);
Retrieve
also changed. It's needs extra provider params.
From
public ReportRetrieveResult Retrieve(ReportRetrieveRequest request)
{
request.CheckNotNull();
if (request.ReportKey.IsEmptyOrNull())
throw new ArgumentNullException("reportKey");
var reportInfo = ReportRegistry.GetReport(request.ReportKey);
if (reportInfo == null)
throw new ArgumentOutOfRangeException("reportKey");
if (reportInfo.Permission != null)
Authorization.ValidatePermission(reportInfo.Permission);
var response = new ReportRetrieveResult();
response.Properties = PropertyItemHelper.GetPropertyItemsFor(reportInfo.Type);
response.ReportKey = reportInfo.Key;
response.Title = reportInfo.Title;
var reportInstance = Activator.CreateInstance(reportInfo.Type);
response.InitialSettings = reportInstance;
response.IsDataOnlyReport = reportInstance is IDataOnlyReport;
response.IsExternalReport = reportInstance is IExternalReport;
return response;
}
To
public ReportRetrieveResult Retrieve(ReportRetrieveRequest request,
IServiceProvider serviceProvider, IPropertyItemProvider propertyItemProvider)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
if (request.ReportKey.IsEmptyOrNull())
throw new ArgumentNullException(nameof(request.ReportKey));
if (propertyItemProvider is null)
throw new ArgumentNullException(nameof(propertyItemProvider));
var reportInfo = ReportRegistry.GetReport(request.ReportKey);
if (reportInfo == null)
throw new ArgumentOutOfRangeException(nameof(request.ReportKey));
if (reportInfo.Permission != null)
Permissions.ValidatePermission(reportInfo.Permission, Localizer);
var response = new ReportRetrieveResult
{
Properties = propertyItemProvider.GetPropertyItemsFor(reportInfo.Type).ToList(),
ReportKey = reportInfo.Key,
Title = reportInfo.Title
};
var reportInstance = ActivatorUtilities.CreateInstance(serviceProvider, reportInfo.Type);
response.InitialSettings = reportInstance;
response.IsDataOnlyReport = reportInstance is IDataOnlyReport;
response.IsExternalReport = reportInstance is IExternalReport;
return response;
}
ReportColumnConverter changes
ReportColumnConverter.ObjectTypeToList
needs 2 extra parameter for work. It's ServiceProvider
and Localizer
. You need add this parameters where it's used in class constructor. Then pass to this method.
Example constructor
protected ITextLocalizer Localizer { get; }
protected IServiceProvider ServiceProvider { get; }
public ExampleClass(ITextLocalizer localizer, IServiceProvider serviceProvider)
{
Localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
From
ReportColumnConverter.ObjectTypeToList(typeof(Item));
To
ReportColumnConverter.ObjectTypeToList(typeof(Item), ServiceProvider, Localizer);
Fixing CustomerGrossSalesReport and SalesByDetailReport like classes
Add constructor first like this.
protected ISqlConnections SqlConnections { get; }
protected ITextLocalizer Localizer { get; }
protected IServiceProvider ServiceProvider { get; }
public CustomerGrossSalesReport(ISqlConnections sqlConnections, ITextLocalizer localizer, IServiceProvider serviceProvider)
{
SqlConnections = sqlConnections ?? throw new ArgumentNullException(nameof(sqlConnections));
Localizer = localizer ?? throw new ArgumentNullException(nameof(localizer));
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
}
Update ReportColumnConverter
usages
From
ReportColumnConverter.ObjectTypeToList(typeof(Item));
To
ReportColumnConverter.ObjectTypeToList(typeof(Item), ServiceProvider, Localizer);
Then add missing using using Serenity;
EmailEndpoint.cs fixes
add extra usings
using Microsoft.Extensions.DependencyInjection;
using Serenity.Abstractions;
Serenity.Authorization
changed to User.Identity
so you need to change it everywhere where you use Serenity.Authorization
.
For Serenity.Authorization.Username
you need to use User?.Identity?.Name
For Serenity.Authorization.UserId
you need to use User?.GetIdentifier()
LocalCache.Get<object>(...)
changed to Context.Cache.Memory.Get<object>(...)
you need change every old usage to new one.
Exception logging has new parameter
From
...
catch (Exception ex)
{
ex.Log();
}
To
...
catch (Exception ex)
{
ex.Log(HttpContext?.RequestServices?.GetService<IExceptionLogger>());
}
ParseMulti
method changed to static
From
private IEnumerable<InternetAddress> ParseMulti(string s, bool ignore = false)
To
private static IEnumerable<InternetAddress> ParseMulti(string s, bool ignore = false)
AttachEmbeddedImages
changed to static and new parameters added
From
private string AttachEmbeddedImages(BodyBuilder builder, string body)
To
private static string AttachEmbeddedImages(BodyBuilder builder, string body, IUploadStorage uploadStorage, PathString pathBase)
You need to change everywhere where VirtualPathUtility.ToAbsolute
used without basePath
like this example
From
string search = src + VirtualPathUtility.ToAbsolute("~/upload/");
To
string search = src + VirtualPathUtility.ToAbsolute(pathBase, "~/upload/");
UploadHelper
changed to UploadPathHelper
.
Reply
and Compose
method has new parameters
From
public EmailReplyResponse Reply(EmailReplyRequest request)
To
public EmailReplyResponse Reply(EmailReplyRequest request, [FromServices] IUploadStorage uploadStorage, [FromServices] IUserRetrieveService userRetriever)
From
public ServiceResponse Compose(EmailComposeRequest request)
To
public ServiceResponse Compose(EmailComposeRequest request, [FromServices] IUploadStorage uploadStorage, [FromServices] IUserRetrieveService userRetriever)
GetAttachmentList
has new parameter pathBase
From
public static AttachmentList GetAttachmentList(MimeMessage message, string folderName, uint uniqueId, bool forReply)
To
public static AttachmentList GetAttachmentList(MimeMessage message, string folderName, uint uniqueId, bool forReply, PathString pathBase)
There is some more changes you can find in EmailEndpoint.cs File in StartSharp template