Skip to content

DotVVM 4.0

Compare
Choose a tag to compare
@exyi exyi released this 23 Mar 18:41
· 58 commits to release/4.0 since this release

DotVVM 4.0 Release notes

Breaking Changes

ordered by severity:

  • We dropped support for IE 11. There is no workaround, except for using some better browser
  • Validation API has changed, see the Validation section for more details. TL;DR: use context.AddModelError(this, vm => vm.EditViewModel.MyProperty)
  • netstandard2.1 or net472 platform is required
  • Some HTTP security headers are enabled by default. See HTTP Security Headers section
  • [Authorize] attribute is now deprecated, please use the context.Authorize() extension method instead. The Authorize attribute still works and we don't plan to remove it, but it's marked obsolete for a reason (see Authorization)
  • Control properties not using the DotvvmProperty.Register or DotvvmPropertyGroup.Register are no longer supported and exception will be thrown.
  • DotvvmControl.SetValue method used to write into the value binding if it was possible. However, this behavior was unreliable so it now always replaces the binding with the new value. If you want the old behavior - to set the value to the binding, use the .SetValueToSource method.
  • Some typos in the public API were fixed (see #1051)
  • Method in server-side styles .SetControlProperty<MyControl>(TheProperty) is replaced by .SetControlProperty(TheProperty, new MyControl()). Also additional using may be required for extension method to be found.
  • DotVVM Error page now only displays errors from DotVVM pages, not from subsequent middlewares. If you are using both DotVVM and other technology in your backend, add the ASP.NET Error Page too.
  • Client-side Repeater uses a separate <template id=...> element instead of placing the template inside. If it poses a problem, it can be configured by setting RenderAsNamedTemplate=false.
  • RouteLink renders the link even in client-side mode.
  • Literal renders the binding even in server-side rendering mode. Use a resource binding to force it to render the value server-side without generating the Knockout binding expression.
  • DotVVM.Diagnostics.StatusPage is now deprecated, the page is now included in DotVVM.Framework package (at _dotvvm/diagnostics/compilation). Please uninstall the package from your project.

New Authorization API

TL;DR: Use context.Authorize(...) instead of [Authorize(...)]. Place it in the OnInit method of your view model or into the respective command/staticCommand. The IDotvvmRequestContext can be injected from DI into your static command service constructor or directly imported in DotHTML using @service context=IDotvvmRequestContext and put into staticCommand argument.

We deprecated the [Authorize] attribute, because it was not called in some unexpected cases (when the command is called from another command, or when the view model class is placed inside another view model). We could fix those few issues, but there would probably always remain some weird cases, so we decided to deprecate the auto-magical invocation ofr the attribute in favor of calling the Authorize method explicitly. It's much easier to debug why your code does not invoke the method than to debug why DotVVM won't call it. Also, when the .Authorize call is placed right next to the logic it's supposed to guard, there is almost no way it wasn't in fact authorized. Since developers rarely write UI tests checking whether auth works properly, we decided calling a method is much safer option for such a critical task.

HTTP Security Headers

Firstly, this DOES NOT magically protect your application from all kinds of attacks. Most common attacks are related to wrongly configured authentication and insuficient user input validation, and none of these headers can help with that. Please remember that everything in your viewmodel which is not marked by the [Protect] attribute is suddently a user input in a command or static command.

Also, we only set headers which are not likely to cause any problems to your application... which, incidentally, are the headers which are less effective 🙃. Please don't forget to set the Strict-Transport-Security yourself and check your application using Mozilla Observatory.

  • X-Frame-Options: DENY is used by default. It should pose no problem, since DotVVM did not work with iframes anyway due to cookie SameSite policy. When CrossSite frames are enabled, the cookie will have SameSite=None

    • Configured in config.Security.FrameOptionsSameOrigin and config.Security.FrameOptionsSameOrigin. Please enable the cross-origin iframes only for routes where you really need it.
  • We also check Sec-Fetch-Dest - if it's an iframe, we validate on server that iframes are allowed. This mostly allows us to print a nicer error message, but may also in theory prevent some timing attacks using iframes

  • X-XSS-Protection: 1; mode=block blocks some kinds of XSS attacks. Probably not super useful, but also not harmful.

    • Configured by config.Security.XssProtectionHeader
  • X-Content-Type-Options: nosniff disables inference of content-type based on content, this could prevent some XSS attacks. Probably also not super useful, but very unlikely to cause any problems.

    • Configured by config.Security.ContentTypeOptionsHeader
  • When on https, we set the session cookie with __Host- prefix. This prevents it being used by subdomains. Can help only in obscure cases - for example when a system on the subdomain is compromised, the attacked can not pivot to the parent domain so easily.

We also check the Sec-Fetch-* headers - that tells us what the browsers intends to do with the page and whether it is a cross-origin or same-origin request. Basically, we don't allow cross-origin POST and SPA requests. We also don't allow JS initiated GET requests to DotVVM pages, use the config.Security.VerifySecFetchForPages option to disable this, if you rely on it.

If the browser does not send these Sec-Fetch-* headers, we don't check anything. You can enable strict checking by config.Security.RequireSecFetchHeaders option. By default it's enabled on compilation page to prevent SSRF and it does not matter too much if it does not work for someone with an obsolete or broken browser.

Improved server-side styles

Server-side styles is a powerful metaprogramming feature available since the early preview versions of DotVVM. However, we have significantly refactored and improved the API in this version to allow access to strongly typed properties.

Most of previous code should remain compatible, except for the SetControlProperty<TControl> method which should be replaced by .SetControlProperty(property, new TControl()).

Styles.Tag DotHTML property is introduced to allow easier matching to specific controls. For example, it may be used like this:

c.Styles.RegisterAnyControl(c => c.HasTag("prepend-icon"))
        .Prepend(new HtmlGenericControl("img").SetAttribute("href", "myicon.png"));

// or this rule applies to any descenant of a tagged control
// it will use <span data-bind="text: ..."></span> instead of knockout virtual element for each literal

c.Styles.Register<Literal>(c => c.HasAncestorWithTag("literal-spans"))
        .SetProperty(c => c.RenderSpanElement, true);

It is now possible to create controls in normal C# code and then put them into children, control properties or append/prepend them to the matched control. Use .PrependContent, .AppendContent, .SetControlProperty, .Append and .Prepend. You can also replace the entire control with a different one using .ReplaceWith which will also copy all properties onto the new control. Last but not least, you can use .WrapWith to place a wrapper control around it.

In previous version, it was only possible to set a constant value into the properties, now it's possible to compute the value for each control using a supplied lambda function:

// this will apply a confirm postback handler to all commands and staticCommands on this control
c.Styles.RegisterAnyControl(c => c.HasTag("confirm"))
    .AppendDotvvmProperty(
        PostBack.HandlersProperty,
        c => new ConfirmPostBackHandler(c.GetHtmlAttribute(
            "data-confirm-msg") ?? "This is a default confirm message")
        );

It is also possible to process a binding in the API. For example, this transforms every usage of the Visible property into a CSS class regardless whether it's a static value or a value / resource binding.

c.Styles.Register<HtmlGenericControl>(c => c.HasProperty(c => c.Visible))
        .SetPropertyGroupMember("Class-", "hide", c =>
            c.Property(c => c.Visible).Negate())
        .SetProperty(c => c.Visible, true); // reset the Visible property

The .Negate() is a extension method defined on the new ValueOrBinding<bool> type. There are many others including .Select(t => ...) where you can supply any lambda function which can be translated to a JavaScript binding.

Validation

We completely reworked the way validation works internally. Validation property paths are now always expressed as absolute paths from the root viewmodel to the affected property. Therefore, property paths always begin with the '/' character (expressing they are starting at the root viewmodel) and this character is also used to delimit individual path segments. Examples:

  • /Customer/Id
    • this means that there is a property Customer in the root viewmodel
    • the error is attached to the property Id on the Customer.
  • /Items/0/Price
    • this means that there is a collection Items in the root viewmodel
    • the error is attached to the element with index 0 of the collection, more specifically, on its element Price
    • note that for indexing in collections, we do not use square brackets

It is now also possible to create custom validation errors and attach them to an object outside of the scope as determined by a validation target. Automatic validation is not affected by this change - only custom errors (by calling, for example Context.AddModelError(...)) can now override validation targets.

In JavaScript console, we now display property paths for each validation error so that violating parts of viewmodel can be easily identified.

Breaking changes:

  • Knockout expression are no longer supported as property paths.
  • It is no longer possible to directly modify contents of the ModelState or individual properties of ViewModelValidationError.
  • The only way to create validation errors is to use one of the .AddModelError(...) methods.
  • We obsoleted the method .AddModelError(string propertyPath, string message) that was previously used to create validation errors using knockout expressions. If you need to specify property paths manually, there is now the .AddRawModelError(string absolutePath, string message) method for this. However, this is intended only for advanced uses as it partially bypasses the validation framework. Other variants (using lambda expressions) should be used whenever possible.
  • Removed the previously obsoleted BuildPath<TValidationTarget> method on the PropertyPath class as it so longer usable.

In order for these changes to be possible, we adjusted API that is used to create validation errors. Whenever manually creating validation errors, users now have to use one of the .AddModelError(...) methods. We obsoleted the AddModelError(string propertyPath, string errorMessage) method.

Control Capabilities

Control capabilities are a way to declare DotVVM control properties in a more concise and reusable way. If you have written a custom control, you know that declaring properties is very verbose. Also, when making a thin wrapper around other controls, it's painful that most of their properties have to be re-declared and then manually copied onto the child control. Capabilities are designed to fix those two problems.

A capability is a normal C# record (or plain data class if you prefer), which has the [DotvvmControlCapability] attribute:

[DotvvmControlCapability]
public sealed record ExampleCapability
{
    public string? MyProperty { get; init; } // will default to null
    public string MyAnotherProperty { get; init; } = "default-value";
}

This declares a reusable set of control properties which any control can then "import" with the following registration:

public static readonly DotvvmProperty ExampleCapabilityProperty =
    DotvvmCapabilityProperty.RegisterCapability<ExampleCapability, MyControl>();

MyControl will now allow both properties from the ExampleCapability: <cc:MyControl MyProperty=A MyAnotherProperty=something />. To access the properties from C# code, you can use the control.GetCapability<ExampleCapability>() method. Alternativelly, to get only one of the properties, use control.GetValue<string>("MyProperty"). To set a capability into a control, contro.SetCapability(x) method is available.

Note that capabilities may contain properties of

  • primitive types - it will be mapped to HTML attributes
  • ITemplate, DotvvmControl, ... - those will be mapped to inner elements
  • other capabilities - will be registered recursively and their properties will be available.

To prevent name clashes and to allow multiple registered capabilities of the same type, you can specify a prefix for all properties in the capability. The prefix is specified as a parameter to the RegisterCapability("prefix") method or as a parameter of the [DotvvmControlCapability("prefix")] attribute (use this when declaring nested capability or when declaring capability for a composite control).

We include HtmlCapability and TextOrContentCapability in the framework

Composite Controls

Simpler way to create code-only controls.

public class ImageButton : CompositeControl
{
    public static DotvvmControl GetContents(
        ValueOrBinding<string> text,
        ICommandBinding click,
        string imageUrl = "/icons/default-image.png"
    )
    {
        return new LinkButton()
            .SetProperty(b => b.Click, click)
            .AppendChildren(
                new HtmlGenericControl("img")
                    .SetAttribute("src", imageUrl),
                new Literal(text)
            );
    }
}

See https://www.dotvvm.com/docs/4.0/pages/concepts/control-development/composite-controls for more information

Roslyn Analyzers

We introduced custom Roslyn Analyzers to help with quick identification of common problems when using DotVVM. Analyzers are packaged within the main DotVVM NuGet package and therefore should be automatically registered by your IDE when referencing the new framework version. These analyzers will notify you about potential problems problems in your code-base by issuing warnings (default behaviour).

Right now, we've implemented a bunch of checks that evaluate whether viewmodels are serializable of not. As a result, the following rules were implemented:

  • DotVVM02 - Use only serializable properties in viewmodels
    • Guard analyzing used properties and custom types to try and determine whether they are JSON-serializable by DotVVM
  • DotVVM03 - Do not use public fields in viewmodels
    • Guard notifying users to use public properties to store the state of viewmodel instead

DotVVM.Testing package

We created new package DotVVM.Testing with helper functions that we previously used internally for testing:

  • DotvvmTestHelper class has static methods CreateConfiguration and CreateContext to create DotvvmConfiguration and IDotvvmRequestContext
  • ControlTestHelper is useful for testing custom controls, you can check out how we use it in CompositeControlTests.cs. TL;DR: there is a RunPage(typeof(MyViewModel), "dothtml page") which executes dotvvm page and returns HTML output parsed using AngleSharp.
  • BindingTestHelper is probably only useful for testing custom method translations, there are methods to create various bindings and compile them to JS.
  • Added control usage validation to GridViewColumn, HtmlLiteral, ComboBox and a few other controls.

Improved Error Page

  • The error page now contains "Save & Share" button which saves it as single HTML file, so it can be shared easily (please include those in bug reports 🙂).
  • A tab with loaded assemblies is added to simplify debugging of broken dependencies.
  • When error occurs in a control, the control hierarchy is shown in the stack trace tab.
  • HttpOnly cookies are redacted for security reasons.
  • In general, we tried to improve error reporting in DotVVM, so please report a bug if it produces a cryptic error even though you are not doing anything super convoluted.

Various stability and performance improvements

We put a lot of effort into fixing bugs and improving performance. Notably, DotVVM apps should consume less memory, startup faster.
It should also produce nicer HTML with less knockout comments.
Generated Javascript was simplified by using the ?., ?? operators and async/await.

Notably, markup controls only include properties which are actually used in value or staticCommand binding to reduce the amount of unnecesary noise in the produced HTML.

Internal refactorings & restructuring

  • We merged MiniProfiler and Application Insights Tracing, Dynamic Data, View HotReload and Compilation Status into this repository. The repository got a new, more reasonable, directory structure.
  • We now use Github Actions for CI instead of Azure DevOoops which leads to much better reliability. You'll also get the same CI Pipelines when you fork the DotVVM repository.
  • We added many new tests using the DotVVM.Testing package, which enables us to do almost end to end testing without relying on Selenium UI tests.
  • src/Tests now executes significantly faster (in less than 10s usually)
  • We figured out how to execute yarn install or npm install from MSBuild, so it should be enough to run dotnet build src/Framework/Framework to compile DotVVM including JS files.

Notable Pull Requests

  • Fix misspellings/typos in public API by @josefpihrt in #1051
  • Refactor server-side styles to make them more type safe and powerful by @exyi in #1056
  • DotVVM error page ignores error from subsequent middlewares by @tomasherceg in #1076
  • Always render everything in RouteLink by @exyi in #1055
  • Add DotVVM.Framework.Testing project by @exyi in #1084
  • Add a switch that lists dotprops to the compiler by @cafour in #1088
  • Enable access to parent extension parameters by @exyi in #1095
    • _parent._index, _parent2._index can now be used to access _index of a parent Repeater when two Repeaters are nested.
  • Fix redirection in staticCommand & SPA by @exyi in #1106
  • Changed default value for Repeater.RenderAsNamedTemplate by @quigamdev in #1101
  • Load even assemblies from DotvvmMarkupConfiguration.Controls by @exyi in #1087
  • Added control usage validation to GridViewColumn by @tomasherceg in #1081
  • Added more JS translations for string methods by @acizmarik in #1100
    • IsNullOrWhiteSpace, Trim(Start/End) and Pad(Left/Right)
  • Cell decorators in GridViewColumn by @tomasherceg in #1077
    • These Decorators can be used to add css classes or other attributes to the <td> element of GridView.
  • Styles: Append, Prepend, Wrap, Replace, Tags by @exyi in #1089
  • Use async/await in static commands by @exyi in #1105
    • Note that this fixes a number of bugs which were present in the staticCommand compiler. If you could not use a staticCommand because of one of these, try it again on this version :)
  • Easier way to write controls by @exyi in #535
    • Added ValueOrBinding<T> type and CompositeControl as a base class for controls which mostly just create other controls.
  • Inline script will use hash ID to identify by @exyi in #1115
  • Use template element instead of script tag by @exyi in #1114
  • Add a MSBuild task for running yarn build by @exyi in #1110
  • Remove support of virtual properties by @exyi in #1111
  • Migrate DotVVM.Framework to nullable references by @exyi in #1118
  • Changed structure of repository based on #1079 by @quigamdev in #1125
  • Add status page into Framework by @cafour in #1119
    • Compilation status page is now part of DotVVM Framework and is enabled automatically in debug configuration. Access it at _dotvvm/diagnostics/compilation
  • Upgrade framework targets by @quigamdev in #1102
  • Remove support of virtual property groups by @exyi in #1124
  • TemplateHost control by @tomasherceg in #1109
    • TemplateHost is a control which renders an ITemplate
  • Recommend users to use integrity hashes on scripts by @exyi in #1126
    • If you are using a script from CDN without an integrity hash, dotvvm will warn you about it.
  • DotvvmProperty aliasing by @cafour in #1099
  • Remove support for Internet Explorer by @exyi in #1142
  • styles: Add helper function for working with children and descendants by @exyi in #1112
  • Working with bindings in server-side styles and CompositeControls by @exyi in #1157
    • Added .Select(c => c + 1) method to ValueOrBinding and IBinding which creates a new binding with a modified expression. Please use this method wisely as it may be a bit slow to create the bindings (although we cache the results).
  • Serialize all properties into the dotvvm_serialized_config.json.tmp by @exyi in #1146
    • Since we have introduced more complex way of registering DotVVM Properties (capability properties and CompositeControls), DotVVM now exports a list of all available properties on startup. This is mainly for the VS extension, but might be useful for debugging too.
  • Add MarkupControlContainer type to allow using markup controls in code controls by @exyi in #1161
    • Use new MarkupControlContainer("myControl:Control", c => c.MyProperty = 1) to initialize a markup control from Code Control or from Server Side Styles.
  • Add helper functions for script & css resources by @exyi in #1159
    • Resources can be registered more easily using new .RegisterScriptFile, .RegisterScriptModuleFile, .RegisterStylesheetFile, ...
  • Fixed an issue with ambiguous serialization of StaticCommandInvocationPlan by @acizmarik in #1166
    • This allows static commands to call an overloaded method
  • Get rid of some knockout comments by @exyi in #1151 and #1153
  • Do not UpdateSource automatically in SetValue by @exyi in #1107
  • Add ReturnFileAsync method by @exyi in #1177
  • static commands: fix parameter passing (in async commands) by @exyi in #1170
  • deprecate Authorize attribute, add Authorize extension method by @exyi in #1174
  • Added JS translations for indexer on readonly collections by @acizmarik in #1193
  • Add helper functions for working with html controls by @exyi in #1198
    • New extension methods are available on all controls with html attributes. These include .AddClass("class"), .AddClass("c", bindingCondition), .SetAttribute(). All of these accept static values, ValueOrBinding and IValueBinding
  • Add translations for Convert.ToX for numeric types by @exyi in #1196
  • Throw if html writer contains attribute and text/end tag is written by @exyi in #1189
  • file upload: Don't put multiple files in collection when AllowMultipleFiles = false by @exyi in #1203
  • Make view precompilation the default in production mode by @exyi in #1210
  • Js component experiment by @exyi in #1158
  • Utilize RecordExceptions by @exyi in #1204
  • Remove obsolete control properties by @exyi in #1211
  • markup controls: Don't render unused properties by @exyi in #1217
    • Markup controls render a with-control-properties bindings. Now only used properties are included in the list and the binding is not rendered at all when no properties are needed.
  • Improve startup and compilation performance slightly by @exyi in #1224
  • Use http security headers by default by @exyi in #1207
  • Rewriting the view compiller to use System.Linq.Expressions by @Mylan719 in #1225
    • The compiler is rewritten to use Linq.Expression instead of generating C# code using Roslyn. It's now significantly faster and more reliable (partly thanks to FastExpressionCompiler)
  • Validation - absolute paths by @MichalTichy in #1145
  • Add Referrer-Policy: no-referrer by default by @exyi in #1229
  • ToBrowserLocalTime implemented by @tomasherceg in #1187
    • This function converts UTC time to browser local time zone. Only works client-side
  • Add net6.0 target and hot reload handler that flushes various caches by @exyi in #1223
    • .NET hot reload should work just fine with serialization, ...
  • ValidationSummary.IncludeErrorsFromTarget set to true by default by @exyi in #1233
  • Fixed and improved serializability analyzers by @acizmarik in #1244
  • Expose in JS if dotvvm is running in debug mode by @exyi in #1245
    • dotvvm.debug in Javascript returns true when dotvvm is running in debug mode.
  • Add CloneTemplate to allow setting templates in server styles by @exyi in #1239
    • Create using new CloneTemplate(new MyControl()). It does a deep clone of the control for each control.
  • Put entire control hierarchy into the error page by @exyi in #1242
  • improve startup and view compilation performance by @exyi in #1243
  • Literal render binding even in server mode by @exyi in #1246
  • Add _js.InvokeAsync method by @exyi in #1262
    • Use _js.InvokeAsync to invoke Javascript methods which return a promise

Full Changelog: v3.2...v4.0.0