todo-it / philadelphia

integrated aspnet core and Bridge.NET crossplatform toolkit for building typesafe single page web applications applications in C#
Apache License 2.0
4 stars 0 forks source link

[!IMPORTANT] as of 2024-03-08 I'm not going to maintain this project anymore


Philadelphia Toolkit

logo

What is this?

Philadelphia Toolkit is an integrated aspnet core and Bridge.NET cross platform toolkit for building type safe web applications in C#.

Tell me more

On the server side it is possible to use other dotnet languages (fact: most of our server side code is written in F#). Code may be shared between server and client (i.e. service contracts, DTOs etc). We strive to keep code concise yet without sacrificing possibility to structure code when it grows. One of the benefits provided by toolkit is that eliminates the need to explicitly invoke JSON serialization/deserialization code (both on server side and client side).

Extensive widgets collection: it provides widgets primarily targeting desktop browsers as its origin lays in LOB apps. We recently added support for mobile browsers by supporting Industrial Android WebApp Host which is webview with additional APIs for photo taking and QR scanning.

Server-sent events - it makes development of services utilizing this tech easy by handling low level concerns away (serialization, deserialization, subscription, canceling subscription, timeouts).

Why calling it toolkit instead of framework? Toolkit assumes that we are helping build tools instead of confining people in 'one true rigid and opinionated' limitations of framework.

Live demo

See Philadelphia.Demo project hosted live on Linux within Docker. Please note that this is cheapest Linux machine available so it is likely that significant demand may cause it to be unavailable due to hug of death.

If you open it with browser it assumes desktop mode. If you open it with Industrial Android WebApp Host it assumes mobile browser.

Installation

Technically speaking you only need dotnet core SDK v2.1 or later installed and dotnet framework 4.5.1 (under Linux you need recent Mono). Why both? Because as of now for compilation of (Bridge v17.10.1) you need 'full dotnet framework'. For runtime it only needs dotnet core.

Install nuget template package:
dotnet new -i Philadelphia.Template

Use that template to create new solution in current directory:
dotnet new phillyapp -n PutYourProjectNameHere

IMPORTANT After using template for the first time you need to perform following actions (otherwise Bridge.NET won't generate JS files):

BUT WHY? This is due to csproj referencing files provided by nuget and NOT present during sln opening. If you won't do it then JS files won't be generated in output folder...

Examples

Example: Hello world

Create new solution as described in Installation section and replace Program.cs content as generated by template with following content (or open Examples/01HelloWorld/HelloWorld.sln):

using Bridge;
using Philadelphia.Web;

namespace PutYourProjectNameHere.Client {
    public class Program {
        [Ready]
        public static void OnReady() {
            Toolkit.InitializeToolkit();
            var renderer = Toolkit.DefaultFormRenderer();

            var msg = new InformationalMessageForm("Hello world");
            msg.Ended += (x, _) => renderer.Remove(x);

            renderer.AddPopup(msg);
        }
    }
}

above yields following outcome in Firefox helloworld.jpg

Example - navigation

Also present under open Examples/02Navigation/Navigation.sln

using System;
using Bridge;
using Bridge.Html5;
using Philadelphia.Common;
using Philadelphia.Web;

namespace PhiladelphiaPowered.Client {
    class MainFormView : IFormView<HTMLElement> {
        public InputTypeButtonActionView ShowInfo {get; } 
            = new InputTypeButtonActionView("Info popup");
        public InputTypeButtonActionView ReplaceMaster {get; } 
            = new InputTypeButtonActionView("Replace master");
        public IView<HTMLElement>[] Actions => ActionsBuilder.For(ShowInfo,ReplaceMaster); //shorter than explicit array

        public RenderElem<HTMLElement>[] Render(HTMLElement parentContainer) {
            return new RenderElem<HTMLElement>[] {"this is main form body<br>using <i>some</i> html tags"};
        }
    }

    class MainForm : IForm<HTMLElement,MainForm,MainForm.Outcome> {
        public string Title => "Main form";
        private readonly MainFormView _view = new MainFormView();
        public IFormView<HTMLElement> View => _view;
        public event Action<MainForm, Outcome> Ended;

        public enum Outcome {
            EndRequested,
            InfoRequested,
            ReplaceMaster
        }

        public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Create(
            () => Ended?.Invoke(this, Outcome.EndRequested));//makes form cancelable

        public MainForm() {
            LocalActionBuilder.Build(_view.ShowInfo, () => Ended?.Invoke(this, Outcome.InfoRequested));
            LocalActionBuilder.Build(_view.ReplaceMaster, () => Ended?.Invoke(this, Outcome.ReplaceMaster));
        }
    }

    class AltMainFormView : IFormView<HTMLElement> {
        public IView<HTMLElement>[] Actions => new IView<HTMLElement>[0];
        public RenderElem<HTMLElement>[] Render(HTMLElement parentContainer) {
            return new RenderElem<HTMLElement>[] {@"that's an alternative main form.<br>
                It has no actions so it's a dead end (press F5 to restart)"};
        }
    }

    class AltMainForm : IForm<HTMLElement,AltMainForm,Unit> {
        public string Title => "Main form";
        public IFormView<HTMLElement> View { get; } = new AltMainFormView();
        public event Action<AltMainForm, Unit> Ended;
        public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Ignore;
    }

    public static class NavigationFlow {
        public static void Run(IFormRenderer<HTMLElement> renderer) {
            var mainFrm = new MainForm();
            var showInfo = new InformationalMessageForm("Some important info", "Info form title");
            var altMainFrm = new AltMainForm();
            renderer.ReplaceMaster(mainFrm);

            mainFrm.Ended += (form, outcome) => {
                switch (outcome) {
                    case MainForm.Outcome.EndRequested:
                        renderer.Remove(form);
                        break;

                    case MainForm.Outcome.InfoRequested:
                        renderer.AddPopup(showInfo);
                        break;

                    case MainForm.Outcome.ReplaceMaster:
                        renderer.ReplaceMaster(altMainFrm);
                        break;
                }
            };

            showInfo.Ended += (form, _) => renderer.Remove(form); //just dismiss this popup (no relevant outcome)
        }
    }

    public class Program {
        [Ready]
        public static void OnReady() {
            Toolkit.InitializeToolkit();
            var renderer = Toolkit.DefaultFormRenderer(LayoutModeType.TitleExtra_Actions_Body);
            NavigationFlow.Run(renderer);
        }
    }    
}

above program as animation in Chrome: validation_flow.gif

Example - simple validation

More sophisticated example that shows how to mix html text with controls and to create actions that are disabled as long as control is not correctly validated.

Also present under open Examples/03SimpleValidation/SimpleValidation.sln

using System;
using Bridge;
using Bridge.Html5;
using Philadelphia.Common;
using Philadelphia.Web;

namespace PutYourProjectNameHere.Client {
    class SomeFormView : IFormView<HTMLElement> {
        public InputTypeButtonActionView Confirm = new InputTypeButtonActionView("OK").With(x => x.MarkAsFormsDefaultButton());
        public IView<HTMLElement>[] Actions => new []{Confirm};
        public InputView Inp = new InputView("Some entry");

        public RenderElem<HTMLElement>[] Render(HTMLElement parent) {
            //notice: you can mix text and controls safely thanks to  implicit conversion operators
            return new RenderElem<HTMLElement>[] {
                "<div style='font-size: 12px'>", 
                    Inp, 
                "</div>"
            };
        }
    }

    class SomeForm : IForm<HTMLElement,SomeForm,CompletedOrCanceled> {
        public string Title => "Example form";
        private readonly SomeFormView _view = new SomeFormView();
        public IFormView<HTMLElement> View => _view;
        public event Action<SomeForm, CompletedOrCanceled> Ended;
        public ExternalEventsHandlers ExternalEventsHandlers => 
            ExternalEventsHandlers.Create(() => Ended?.Invoke(this, CompletedOrCanceled.Canceled));
//comment out above declaration and uncomment next line to make form noncloseable
//public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Ignore;

        public SomeForm() {
            var inp = LocalValueFieldBuilder.Build(_view.Inp, 
                (v, errors) => errors.IfTrueAdd(string.IsNullOrWhiteSpace(v), 
                    "Must contain at least one non whitespace character"));

            var conf = LocalActionBuilder.Build(_view.Confirm, 
                () => Ended?.Invoke(this, CompletedOrCanceled.Completed));
            conf.BindEnableAndInitializeAsObserving(x => x.Observes(inp));
        }
    }

    public class Program {
        [Ready]
        public static void OnReady() {
            Toolkit.InitializeToolkit();
            var renderer = Toolkit.DefaultFormRenderer();

            var msg = new SomeForm();

            msg.Ended += (x, outcome) => renderer.Remove(x); 
            //outcome? for this form it's either Completed or Cancelled. For simplicity we don't do anything with it

            renderer.AddPopup(msg);
            //comment out former line and uncomment next line to achieve frameless
            //renderer.ReplaceMaster(msg); 
        }
    }
}

above yields following outcome in Firefox validation_dailog.png

In frameless mode it looks this way: validation_chromeless.png

Notice how form title is placed differently without you doing anything. Same for exit action. If you change line
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Create(() => Ended?.Invoke(this,CompletedOrCanceled.Canceled)); to
public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Ignore;

... then those different dismiss form actions will disappear ('X' in dialog or 'door exit' in frameless).

Example - scanning QRs and taking photos

TODO prepare example based on demo code

Example - calling server

Create project using template as instructed in Installation section above. As a result you will have simplest server calling example:

NOTE: if you change or add anything in interfaces decorated with [HttpService] attribute you need to run dotnet script Services.csx as otherwise your project won't compile.

Example of custom widget (IView implementation)

Also present under open Examples/04CustomIView/CustomIView.sln

using System;
using System.Collections.Generic;
using System.Linq;
using Bridge;
using Bridge.Html5;
using Philadelphia.Common;
using Philadelphia.Web;

namespace PutYourProjectNameHere.Client {
    //first lets define some nonobvious custom class that will be presented by IView
    public class Article {
        public bool IsBreakingNews {get; set;}
        public string Title {get; set;}
        public string Author {get; set;}
        public DateTime PublishedAt {get; set;}
        public string Body {get; set;}
    }

    //this is a custom IView that is used to render Article
    public class ArticleReadOnlyValueView : IReadOnlyValueView<HTMLElement,Article> {
        private readonly HTMLDivElement _widget 
            = new HTMLDivElement {ClassName = typeof(ArticleReadOnlyValueView).Name };
        private Article _lastValue;
        public event UiErrorsUpdated ErrorsChanged;
        public ISet<string> Errors => DefaultInputLogic.GetErrors(_widget);
        public HTMLElement Widget => _widget;

        public Article Value {
            get => _lastValue;
            set {
                _lastValue = value;
                _widget.RemoveAllChildren();

                if (value == null) {
                    return; //normally you would never need this...
                }
                _widget.AddOrRemoveClass(value.IsBreakingNews, "isBreakingNews");
                _widget.AppendChild(new HTMLDivElement {
                    TextContent = $"{(value.IsBreakingNews ? "Breaking news: " : "")}{value.Title}" });

                _widget.AppendChild(new HTMLDivElement { 
                    TextContent = $@"by {value.Author} published at 
                        {I18n.Localize(value.PublishedAt, DateTimeFormat.YMDhm)}" });

                _widget.AppendChild(new HTMLDivElement {TextContent = value.Body});
            }
        }

        public void SetErrors(ISet<string> errors, bool causedByUser)  {
            _widget.SetAttribute(Magics.AttrDataErrorsTooltip, string.Join("\n", errors));
            _widget.Style.BackgroundColor = errors.Count <= 0 ? "" : "#ff0000";
            ErrorsChanged?.Invoke(this, errors);
        }

        //FormView->Render() is short thanks to this
        public static implicit operator RenderElem<HTMLElement>(ArticleReadOnlyValueView inp) {
            return RenderElem<HTMLElement>.Create(inp);
        }
    }

    //due to custom control following FormView is merely instantiating controls (and has almost no logic on its own)
    public class NewsReaderFormView : IFormView<HTMLElement> {
        public ArticleReadOnlyValueView NewsItem {get; } = new ArticleReadOnlyValueView();
        public InputTypeButtonActionView NextItem {get;} 
            = InputTypeButtonActionView.CreateFontAwesomeIconedButtonLabelless(Magics.FontAwesomeReloadData)
                .With(x => x.Widget.Title = "Fetch next newest news item");
        public IView<HTMLElement>[] Actions => ActionsBuilder.For(NextItem);

        public RenderElem<HTMLElement>[] Render(HTMLElement parentContainer) {
            return new RenderElem<HTMLElement>[] {NewsItem};
        }
    }

    //news reader form that informs outside world that user requests next news item or has enough news for today
    public class NewsReaderForm : IForm<HTMLElement,NewsReaderForm,NewsReaderForm.ReaderOutcome> {
        public enum ReaderOutcome {
            FetchNext,
            Cancelled
        }
        public string Title => "News reader";
        private readonly NewsReaderFormView _view = new NewsReaderFormView();
        public IFormView<HTMLElement> View => _view;
        public event Action<NewsReaderForm,ReaderOutcome> Ended;
        public ExternalEventsHandlers ExternalEventsHandlers => ExternalEventsHandlers.Create(
            () => Ended?.Invoke(this, ReaderOutcome.Cancelled));

        public NewsReaderForm() {
            LocalActionBuilder.Build(_view.NextItem, () => Ended?.Invoke(this, ReaderOutcome.FetchNext));
        }

        public void Init(Article itm) {
            _view.NewsItem.Value = itm;
        }
    }

    //helper extensions
    public static class ArrayExtensions {
        public static T RandomItem<T>(this T[] self) {
            var i = DateTime.Now.Second % self.Length;
            return self[i];
        }

        public static IEnumerable<T> PickSomeRandomItems<T>(this T[] self, int cnt) {
            var x = DateTime.Now.Second % self.Length;
            return Enumerable.Range(0, cnt).Select(i => self[(x+i) % self.Length]);
        }
    }

    //entry point
    public class Program {
        [Ready]
        public static void OnReady() {
            var di = new DiContainer();
            Services.Register(di); //registers discovered services from model

            Toolkit.InitializeToolkit();
            var renderer = Toolkit.DefaultFormRenderer();

            var reader = new NewsReaderForm();
            reader.Init(GenerateNewsItem());

            reader.Ended += (x, outcome) => {
                switch (outcome) {
                    case NewsReaderForm.ReaderOutcome.Cancelled:
                        renderer.Remove(x);
                        break;

                    case NewsReaderForm.ReaderOutcome.FetchNext:
                        x.Init(GenerateNewsItem());
                        break;
                }
            };

            renderer.AddPopup(reader);
        }

        private static string[] _fnames = {"Anna", "John","Mike", "Paul", "Frank", "Mary", "Sue" };
        private static string[] _lnames = {"Doe", "Smith", "Tomatoe", "Potatoe", "Eggplant" };
        private static string[] _title1 = {"Cow", "Famous actor","Famous actress", "UFO", "Popular social platform", "Alien", "Cat", "President", "Dog", "Big company CEO", "Law abiding citizen", "Old man", "Young lady" };
        private static string[] _title2 = {"ate", "was run over by", "was eaten by", "was surprised by", "stumbled upon" };
        private static string[] _title3 = {"decent folks", "whole nation", "neighbour", "Internet", "MEP", "galaxy", "homework", "its fan", "conspiracy theorist crowd" };
        private static string[] _body = {"It was completely unexpected.", "It came as a shock to everybody in country", 
            "Whole nation is in shock.", "President calls for special powers to address unexpected situation.", 
            "Stock exchange is in panic.", "Majority of MDs call it pandemia.", "Will we ever be able to cope with such tragedy?",
            "Major social platforms provides special tools to help its users deal with a tragedy."};

        private static Article GenerateNewsItem() {
            return new Article {
                IsBreakingNews = DateTime.Now.Second % 2 == 0,
                PublishedAt = DateTime.Now,
                Author = $"{_fnames.RandomItem()} {_lnames.RandomItem()}",
                Title = $"{_title1.RandomItem()} {_title2.RandomItem()} {_title3.RandomItem()}",
                Body = string.Join("", _body.PickSomeRandomItems(4))
            };
        }
    }
}

additionally paste following code into the header of index-*.html file(s).

    <style type="text/css">
        .ArticleReadOnlyValueView {
            display: flex;
            flex-direction: column;
            width: 50vw;
        }

        .ArticleReadOnlyValueView > div:nth-child(1) {
            font-weight: bold;
            font-size: larger;
        }

        .ArticleReadOnlyValueView.isBreakingNews > div:nth-child(1) {
            color: red;
        }

        .ArticleReadOnlyValueView > div:nth-child(2) {
            color: gray;
        }

        .ArticleReadOnlyValueView > div:nth-child(3) {
            flex-grow: 1;
        }
    </style>

This will generate following outcome: custom_iview.gif

Example of Dependency Injection

Once you start building richer programs you will likely notice that providing f.e. service implementations to IFlows is tedious. DiContainer is a solution to this problem. Instead of writing this way:

namespace DependencyInjection.Domain {
    public class SomeForm : IForm<HTMLElement,SomeForm,Unit> {
        public SomeForm(IHelloWorldService someService) {
            //here would be some construction body...
        }

        //rest of IForm implementation would be present here...
    }

    public class SomeFlow : IFlow<HTMLElement> {
        private readonly SomeForm _someForm;

        public SomeFlow(IHelloWorldService helloService, SomeForm someForm) {
            _someForm = someForm;
            //here would be rest of constructor code...
        }

        public void Run(IFormRenderer<HTMLElement> renderer, Action atExit) {
            //here would be inter-form navigational logic...
        }
    }

    public class ProgramWithoutRichDi {
        [Ready]
        public static void OnReady() {
            var di = new DiContainer();
            Services.Register(di); //registers discovered services from model

            Toolkit.InitializeToolkit();
            var renderer = Toolkit.DefaultFormRenderer();

            var helloService = di.Resolve<IHelloWorldService>();

            //REMEMBER NEXT LINES...
            new SomeFlow(
                helloService, 
                new SomeForm(helloService)
            ).Run(renderer); //here you are providing whole dependency tree manually
        }
    }
}

write this instead:

namespace DependencyInjection.Domain {
    public class SomeForm : IForm<HTMLElement,SomeForm,Unit> {
        public SomeForm(IHelloWorldService someService) {
            //here would be some construction body...
        }

        //rest of IForm implementation would be present here...
    }

    public class SomeFlow : IFlow<HTMLElement> {
        private readonly SomeForm _someForm;

        public SomeFlow(IHelloWorldService helloService, SomeForm someForm) {
            _someForm = someForm;
            //here would be rest of constructor code...
        }

        public void Run(IFormRenderer<HTMLElement> renderer, Action atExit) {
            //here would be inter-form navigational logic...
        }
    }

    public class ProgramWithoutRichDi {
        [Ready]
        public static void OnReady() {
            var di = new DiContainer();
            Services.Register(di); //registers discovered services from model

            di.Register<SomeFlow>(LifeStyle.Transient);
            di.Register<SomeForm>(LifeStyle.Transient);

            Toolkit.InitializeToolkit();
            var renderer = Toolkit.DefaultFormRenderer();

            //...COMPARE WITH FOLLOWING FILE
            di.Resolve<SomeFlow>().Run(renderer); //DI container builds SomeFlow instance itself
        }
    }
}

This example is present under Examples/05DependencyInjection/DependencyInjection.sln

What are the advantages? Your code is generally shorter the more you leverage this pattern. Then you don't need to change all new SomeClass invocations every time you change constructor signature -> it gives you more flexibility and saves you tedious changes.

NOTE: you can use same dependency injection API on server and on client. On the server implementation delegates to ASP.NET core DI. On the client it uses toolkit provided implemnentation that internally relies on Reflection API. One thing missing on the client currently is LifeTime.Scoped support (Transient and Singleton work fine). Also notice that API lets you register several implementations for the same key. When you call Resolve<T>() you get the first registered implementation. If you call ResolveAll<T>() you will get all instances.

Example to use I18n, UploadView, datetime pickers, Server-sent events and many others

To see way more demos f.e. for grid clone this repo and open Philadelphia.Toolkit.And.Demo.sln. This is source of live demo mentioned in the beginning.

Solution components

Assuming that you've used template to generate project named 'ExampleApp' you will get following solution:

dd

License info

Everything is covered by Apache 2.0 with the exception of third party font and icons resources. In other words: all source code and CSS scripts are licensed under Apache 2.0. Specifically following files are third party and licensed otherwise:

Images and pdf files in Philadelphia.Demo.Client\ImagesForUploadDemo\Full and Philadelphia.Demo.Client\ImagesForUploadDemo\Thumb that are used for demo of UploadView. Dominik Pytlewski is its sole author. All those files are in public domain - Dominik doesn't claim any copyright on those files.

Contributing

You are welcome to contribute! If you find a bug report it via issue please. If you know how to fix it then please provide a fix by:

If you would like to implement new feature please create an issue first so that we can discuss it.

More details

JSON serialization/deserialization:

What does 'Rich library of widgets' actually means:

MVP pattern

Tookit uses Model-View-Presenter UI architectural pattern to achieve separation of concerns. Business logic resides in rich model. View should only be concerned with visual needs. Generally there's an assumption that you have variables in code that are:

Upon change they validate themselves and raise events so that others may act. That's your Model.

On the other hand there are widgets (DOM elements wrapped so that they expose similar observable interfaces as Model). They expose events so that one can be notified that view was changed (by user or programmatically). Now toolkit has means to bilaterally connect model with view so that:

In other words we have rich databinding:

Internal abstractions used

public interface IReadOnlyValue<ValueT> {
    ValueT Value { get; }
    IEnumerable<string> Errors { get; }

    event ValueChangedRich<ValueT> Changed;
}

public delegate void Validate<T>(T newValue, ISet<string> errors);

public interface IReadWriteValue<ValueT> : IReadOnlyValue<ValueT> {
    event Validate<ValueT> Validate;

    void Reset(bool isUserChange = false, object sender = null);
    Task<Unit> DoChange(ValueT newValue, bool isUserChange, object sender=null, bool mayBeRejectedByValidation=true); //due to user input or programmatic
}
public interface IView<WidgetT> {
    WidgetT Widget { get; }
}

public delegate void UiErrorsUpdated(object sender, ISet<string> errors);

public interface IReadOnlyValueView<WidgetT,ValueT> : IView<WidgetT> {
    event UiErrorsUpdated ErrorsChanged;

    /// <summary>doesn't cause IReadWriteValueView->Changed to be raised</summary>
    ValueT Value { get; set; }
    ISet<string> Errors { get; }
    void SetErrors(ISet<string> errors, bool causedByUser); //includes validation,conversion and remote save errors
}

View variable is declared in line:

public InputView Inp = new InputView("Some entry");

In this line is a 'combo' that creates: Model variable, binds it to View variable _view.Inp and attaches validator

var inp = LocalValueFieldBuilder.Build(_view.Inp, 
                (v, errors) => errors.IfTrueAdd(string.IsNullOrWhiteSpace(v), 
                    "Must contain at least one non whitespace character"));

And here's another combo - this time for 'action'. It creates: Model for View that is clickable only when inp is properly validated

var conf = LocalActionBuilder.Build(_view.Confirm, 
                () => Ended?.Invoke(this, CompletedOrCanceled.Completed));
            conf.BindEnableAndInitializeAsObserving(x => x.Observes(inp));

The only abstractions left that you should use for succinct code are:

Dependency injection on server side

Server side needs to be given references to assemblies that contain service interfaces and its implementations. During server start it looks for classes implementing IDiInstaller interfaces to instance them and invoke their sole Install method. It serves same purpose as in Windsor installer. On client side simple DiContainer is also available.

Service discovery / Conventions in service methods

Toolkit supports several scenarios on the server:

Additionally on the server you can use following filter to do some AOP tasks such as logging requests, blocking requests, authentication, db transactions, etc.

interface Philadelphia.Server.Common.ILifetimeFilter {
    Task OnServerStarted(IDiResolveReleaseOnlyContainer di);

    //depending on result connection will continue or be rejected
    Task<ConnectionAction> OnConnectionBeforeHandler(IDiResolveReleaseOnlyContainer di, string url, object serviceInstance,  MethodInfo m, ResourceType resource);
}

    ///not invoked for static resources
    ///not invoked when OnConnectionBeforeHandler returned filter. 
    ///null Exception means success.
    Task OnConnectionAfterHandler(object connectionCtx, IDiResolveReleaseOnlyContainer di, Exception exOrNull);

More specifically
Decorate your service interfaces with single marker attribute [HttpService] and toolkit will understand that you want it to expose methods in that interface:

When you follow those conventions then not only server side part of toolkit discovers your services but also client side code generation builds for you code compatible with:

What does it do for regular GET&POST method?

For Server-sent events methods:

Additionally regular POST methods may also deal with file uploads and downloads. When POST method should return file (as attachment or inline) it should return instance of Philadelphia.Common.FileModel. It can return files made 'on fly' or otherwise created.

For uploads convention is used that your method needs to have signature:

//method name must end with 'Setter', have following  first parameter and following result. You can add other parameters if needed
Task<RemoteFileId[]> SomeFancyNameSetter(UploadInfo info, int myFirstParam, bool myOtherParam);

For downloads convention is:

Task<FileModel> WhateverName(int yourArbitrary, object parametersIfNeeded);

BUT if you create pair of getter and setter

        Task<FileModel> SomethingGetter(RemoteFileId fileIdentifier, SomeType  someVal /* more params if needed*/);
        Task<RemoteFileId[]> SomethingSetter(UploadInfo info, SomeType  someVal /* more params if needed*/);

then such pair may be used together with Philadelphia.Web.UploadView upload&download widget. See demo code on how to use it.

If there is any [HttpService] without implementation - you get crash during startup (we don't like runtime errors if we can detect bugs earlier).

Static files (images, css, compiled js file etc) are declared to be exposed publicly by using static_resources.json file(s) such as this one:

[
    {
        "Url": "/",
        "FileSystemPath": "../PhiladelphiaPowered.Client/index-min.html",
        "MimeType": "text/html"
    },
    {
        "Url": "/{0}",
        "Dir": "../PhiladelphiaPowered.Client/Bridge/output",
        "FilePattern": "(?i)\\.js$",
        "MimeType": "application/javascript"
    },
    {
        "Url": "/{0}",
        "Dir": "../packages/Philadelphia.StaticResources.*/content",
        "FilePattern": "(?i)\\.css$",
        "MimeType": "text/css"
    },
    //etc
]

NOTE: last line contains '*' - toolkit is able to find 'sole matching directory'. If it finds zero or more than one then it will throw userfriendly exception during start NOTE: paths are relative to that json file location. Static resource json file(s) are loaded once at startup of server for better security and performance (sacrificing some memory).

Remarks

Security

This is general info only. We don't guarantee anything. See license for more details

Authors

Dominik Pytlewski and Tomasz Sztokinier We are two guys behind TODO IT Spółka z o.o. in Warsaw, Poland - limited company.

History

Originally developed as proof of concept in 2015. It is loosely inspired by GWT project. We were heavy users of that project in 2008-2013. In 2014 Microsoft started changing its countenance by opening up to its users. We abandoned Java as it seemed stagnated and opted for dotnet in Mono hoping for the better long term future. We yearned for integrated, statically typed, code shareable environment between those two. We wanted to utilize more powerful C# language and new browser abilities. At that time toolkit utilized Saltarelle compiler and NancyFx to be able to host it in Mono under Linux. In 2016 it was ported to Bridge.NET. Later that year our first commercial project was started. In 2018 after few successful projects we ported it to dotnet core and aspnet core. We did it to bring better crossplatform support, better performance and to bring SSE support.

In future we expect that once WebAssembly matures together with CoreRT AOT's webassembly compilation target support it should be possible to also target CoreRT+WebAssembly instead of (only) BridgeDotNet+JavaScript.