Exemple #1
0
        public void FileSerialization()
        {
            var fms          = new FileMessageStore(new[] { TestFileFolders.FilesAbsPath }, new[] { Guid.NewGuid().ToString("N") }, Encoding.UTF8);
            var tempFilename = Path.GetTempFileName();

            fms.Serialize(tempFilename, Encoding.UTF8);
            Assert.True(fms.Equals(FileMessageStore.Deserialize(tempFilename, Encoding.UTF8)));
            File.Delete(tempFilename);
        }
Exemple #2
0
        public void StreamSerialization()
        {
            var fms    = new FileMessageStore(new[] { TestFileFolders.FilesAbsPath }, new[] { Guid.NewGuid().ToString("N") }, Encoding.UTF8);
            var stream = new MemoryStream();

            fms.Serialize(stream, Encoding.UTF8);
            stream.Position = 0;
            var restoredFms = FileMessageStore.Deserialize(stream, Encoding.UTF8);

            Assert.True(fms.Equals(restoredFms));

            Assert.AreEqual(fms.GetHashCode(), restoredFms.GetHashCode());
            Assert.False(fms.Equals(null));
            Assert.True(fms.Equals(fms));
            Assert.False(fms.Equals(new object()));
        }
Exemple #3
0
        /// <summary>
        /// This method gets called by the runtime. Use this method to add services to the container.
        /// </summary>
        /// <param name="services"></param>
        public void ConfigureServices(IServiceCollection services)
        {
            // Add services required for using options.
            services.AddOptions();

            #region * DataProtection service configuration *

            // Usage:
            // private readonly IDataProtector protector;
            // public SomeController(IDataProtectionProvider provider)
            // {  protector = provider.CreateProtector("isolation purpose");}
            // public IActionResult Test(string input)
            // { var protectedPayload = protector.Protect(input);
            // var unprotectedPayload = protector.Unprotect(protectedPayload)
            // ...}

            // required for cookies and session cookies (will throw CryptographicException without)
            services.AddDataProtection()
            .SetApplicationName("League")
            .SetDefaultKeyLifetime(TimeSpan.FromDays(360))
            .PersistKeysToFileSystem(
                new DirectoryInfo(Path.Combine(WebHostEnvironment.ContentRootPath, "DataProtectionKeys")))
            .UseCryptographicAlgorithms(new AuthenticatedEncryptorConfiguration()
            {
                EncryptionAlgorithm = EncryptionAlgorithm.AES_256_CBC,
                ValidationAlgorithm = ValidationAlgorithm.HMACSHA256
            });

            // register the multi purpose DataProtector
            services.AddSingleton(typeof(League.DataProtection.DataProtector));

            #endregion

            // Configure form upload limits
            services.Configure <Microsoft.AspNetCore.Http.Features.FormOptions>(fo =>
            {
                fo.ValueLengthLimit         = int.MaxValue;
                fo.MultipartBodyLengthLimit = int.MaxValue;
            });

            // The default region of this app is "us", unless configured differently
            // The region info is used for country-specific data like phone numbers
            var regionInfo = new RegionInfo(Configuration.GetSection("RegionInfo").Value ?? "us");
            services.AddSingleton <RegionInfo>(regionInfo);

            // The default culture of this app is "en". Supported cultures: en, de
            CultureInfo.DefaultThreadCurrentCulture   = new CultureInfo(Configuration.GetSection("CultureInfo:Culture").Value ?? $"en-{regionInfo.TwoLetterISORegionName}");
            CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(Configuration.GetSection("CultureInfo:UiCulture").Value ?? $"en-{regionInfo.TwoLetterISORegionName}");

            // DO NOT USE `options => options.ResourcesPath = "..."` because then resource files in other locations won't be recognized (e.g. resx in the same folder as the controller class)
            services.AddLocalization();

            var dbContextList = DbContextList.DeserializeFromFile(Path.Combine(WebHostEnvironment.ContentRootPath, Program.ConfigurationFolder, $"DbContextList.{WebHostEnvironment.EnvironmentName}.config"));
            foreach (var dbContext in dbContextList)
            {
                dbContext.ConnectionString = Configuration.GetConnectionString(dbContext.ConnectionKey);
            }

            var dbContextResolver = new DbContextResolver(dbContextList);
            services.AddSingleton <OrganizationContextResolver>(s => new OrganizationContextResolver(dbContextResolver, s.GetRequiredService <ILogger <OrganizationContextResolver> >(), Path.Combine(WebHostEnvironment.ContentRootPath, Program.ConfigurationFolder)));

            services.AddSingleton <SiteList>(sp =>
                                             SiteList.DeserializeFromFile(Path.Combine(WebHostEnvironment.ContentRootPath,
                                                                                       Program.ConfigurationFolder, $"SiteList.{WebHostEnvironment.EnvironmentName}.config")));
            services.AddScoped <SiteContext>();

            services.Configure <IISOptions>(options => { });

            services.AddSingleton <IHttpContextAccessor, HttpContextAccessor>();
            services.AddSingleton <IActionContextAccessor, ActionContextAccessor>();
            // Make UrlHelper injectable to any component in the HttpContext
            services.AddScoped <IUrlHelper>(sp => {
                var actionContext = sp.GetRequiredService <IActionContextAccessor>().ActionContext;
                var factory       = sp.GetRequiredService <IUrlHelperFactory>();
                return(factory.GetUrlHelper(actionContext));
            });

            services.AddSingleton <IConfiguration>(Configuration);

            services.AddMemoryCache(); // Adds a default in-memory cache implementation
            // MUST be before AddMvc!
            services.AddSession(options =>
            {
                options.IdleTimeout        = TimeSpan.FromMinutes(60);
                options.Cookie.HttpOnly    = true;
                options.Cookie.Name        = ".sid";
                options.Cookie.IsEssential = true;
            });

            #region ** Identity and Authentication **

            var socialLogins = Configuration.GetSection(nameof(SocialLogins)).Get <SocialLogins>();
            services.AddAuthentication(options =>
            {
                options.DefaultScheme             = IdentityConstants.ApplicationScheme;
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme    = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme       = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddFacebook(options =>
            {
                options.AppId        = socialLogins.Facebook.AppId;
                options.AppSecret    = socialLogins.Facebook.AppSecret;
                options.CallbackPath = "/signin-facebook";     // this path is used by the middleware only, no route necessary
                // add the facebook picture url as an additional claim
                options.ClaimActions.MapJsonKey("urn:facebook:picture", "picture", "picture.data.url");
                options.SaveTokens             = true;
                options.CorrelationCookie.Name = ".CorrAuth.League";
                options.Events.OnRemoteFailure = context =>
                {
                    // Note: If this delegate is missing, errors with the external login lead to a System.Exception: access_denied;Description=Permissions error
                    var qsParameter =
                        new Dictionary <string, string>
                    {
                        { "remoteError", context.Request.Query["error"] },
                    }.Where(item => !string.IsNullOrEmpty(item.Value)).ToDictionary(i => i.Key, i => i.Value);
                    // joins query strings from RedirectUri and qsParameter
                    var redirectUri = QueryHelpers.AddQueryString(context.Properties?.RedirectUri ?? "/", qsParameter);
                    context.Response.Redirect(redirectUri);
                    context.HandleResponse();
                    return(Task.CompletedTask);
                };
            })
            .AddGoogle(options =>
            {
                options.ClientId     = socialLogins.Google.ClientId;
                options.ClientSecret = socialLogins.Google.ClientSecret;
                options.CallbackPath = "/signin-google";     // this path is used by the middleware only, no route necessary
                options.ClaimActions.MapJsonKey("urn:google:picture", "picture", "url");;
                options.SaveTokens             = true;
                options.CorrelationCookie.Name = ".CorrAuth.League";
                options.Events.OnRemoteFailure = context =>
                {
                    // Note: If this delegate is missing, errors with the external login lead to a System.Exception: access_denied;Description=Permissions error
                    var qsParameter =
                        new Dictionary <string, string>
                    {
                        { "remoteError", context.Request.Query["error"] },
                    }.Where(item => !string.IsNullOrEmpty(item.Value)).ToDictionary(i => i.Key, i => i.Value);
                    // joins query strings from RedirectUri and qsParameter
                    var redirectUri = QueryHelpers.AddQueryString(context.Properties?.RedirectUri ?? "/", qsParameter);
                    context.Response.Redirect(redirectUri);
                    context.HandleResponse();
                    return(Task.CompletedTask);
                };
            })
            .AddMicrosoftAccount(options =>
            {
                options.ClientId               = socialLogins.Microsoft.ClientId;
                options.ClientSecret           = socialLogins.Microsoft.ClientSecret;
                options.CallbackPath           = "/signin-microsoft"; // this path is used by the middleware only, no route necessary
                options.SaveTokens             = true;
                options.CorrelationCookie.Name = ".CorrAuth.League";
                options.Events.OnRemoteFailure = context =>
                {
                    // Note: If this delegate is missing, errors with the external login lead to a System.Exception: access_denied;Description=Permissions error
                    var qsParameter =
                        new Dictionary <string, string>
                    {
                        { "remoteError", context.Request.Query["error"] },
                    }.Where(item => !string.IsNullOrEmpty(item.Value)).ToDictionary(i => i.Key, i => i.Value);
                    // joins query strings from RedirectUri and qsParameter
                    var redirectUri = QueryHelpers.AddQueryString(context.Properties?.RedirectUri ?? "/", qsParameter);
                    context.Response.Redirect(redirectUri);
                    context.HandleResponse();
                    return(Task.CompletedTask);
                };
            });

            // Add before Application and External Cookie configuration and ConfigureApplicationCookie
            services.AddIdentity <ApplicationUser, ApplicationRole>(options =>
            {
                Configuration.Bind("IdentityOptions", options);     // bind to IdentityOptions section of appsettings.json
            })
            .AddDefaultTokenProviders()
            .AddUserStore <UserStore>()
            .AddRoleStore <RoleStore>()
            .AddErrorDescriber <MultiLanguageIdentityErrorDescriber>()
            .AddUserValidator <LeagueUserValidator <ApplicationUser> >();  // on top of default user validator

            // Make custom claims be added to the ClaimsPrincipal
            services.AddScoped <IUserClaimsPrincipalFactory <ApplicationUser>, LeagueClaimsPrincipalFactory>();

            // Defines the lifetime of tokens sent to users for email / password confirmation et al
            services.Configure <DataProtectionTokenProviderOptions>(options =>
                                                                    options.TokenLifespan = TimeSpan.FromDays(3)); // default: 1 day

            // Add for required user name length et al
            services.Configure <LeagueUserValidatorOptions>(Configuration.GetSection(nameof(LeagueUserValidatorOptions)));

            #endregion

            #region *** Authorization ***

            services.AddAuthorization(options =>
            {
                // Used on controller method level
                options.AddPolicy(Authorization.PolicyName.MatchPolicy, policy => policy.RequireRole(Identity.Constants.RoleName.SystemManager, Identity.Constants.RoleName.TournamentManager, Identity.Constants.RoleName.TeamManager));
                // Used in team views
                options.AddPolicy(Authorization.PolicyName.SeeTeamContactsPolicy, policy => policy.RequireRole(Identity.Constants.RoleName.SystemManager, Identity.Constants.RoleName.TournamentManager, Identity.Constants.RoleName.TeamManager, Identity.Constants.RoleName.Player));
                // Used for my team views
                options.AddPolicy(Authorization.PolicyName.MyTeamPolicy, policy => policy.RequireRole(Identity.Constants.RoleName.SystemManager, Identity.Constants.RoleName.TournamentManager, Identity.Constants.RoleName.TeamManager, Identity.Constants.RoleName.Player));
                options.AddPolicy(Authorization.PolicyName.MyTeamAdminPolicy, policy => policy.RequireRole(Identity.Constants.RoleName.SystemManager, Identity.Constants.RoleName.TournamentManager));
            });

            // Handler for match date, venue and result authorization
            services.AddSingleton <IAuthorizationHandler, Authorization.MatchAuthorizationHandler>();
            // Handler for team, team venue, team members authorization
            services.AddSingleton <IAuthorizationHandler, Authorization.TeamAuthorizationHandler>();
            // Handler for venue authorization
            services.AddSingleton <IAuthorizationHandler, Authorization.VenueAuthorizationHandler>();

            #endregion

            #region ** Application and External Cookie configuration **

            services.Configure <SecurityStampValidatorOptions>(options =>
            {
                // This determines the time span after which the validity of the authentication cookie
                // will be checked against persistent storage. It is accomplished by calling the
                // SecurityStampValidator for every request to the server. If the current time minus the
                // cookie's issue time is less or equal to ValidationInterval, a call to
                // SignInManager<TUser>.ValidateSecurityStampAsync will occur. This means ValidationInterval = TimeSpan.Zero
                // leads to calling the ValidateSecurityStampAsync for each request.
                options.ValidationInterval = TimeSpan.FromMinutes(15); // default: 30 minutes
            });

            services.ConfigureApplicationCookie(options =>
            {
                // Cookie settings
                options.Cookie.HttpOnly     = true;
                options.Cookie.IsEssential  = true;
                options.CookieManager       = new LeagueCookieManager();
                options.Cookie.Name         = ".Auth"; // will be set by LeagueCookieManager
                options.Cookie.Path         = "/";     // may be set by LeagueCookieManager
                options.LoginPath           = new PathString("/account/sign-in");
                options.LogoutPath          = new PathString("/account/sign-in");
                options.AccessDeniedPath    = new PathString("/error/access-denied");
                options.Cookie.SameSite     = Microsoft.AspNetCore.Http.SameSiteMode.Lax; // don't use Strict here
                options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
                options.ExpireTimeSpan      = TimeSpan.FromDays(30);
                options.SlidingExpiration   = true;

                options.Events.OnRedirectToAccessDenied = context =>
                {
                    var returnUrl = "?ReturnUrl=" + context.Request.Path + context.Request.QueryString;
                    // fires with [Authorize] attribute, when the user is authenticated, but does not have enough privileges
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    // other context properties can be set, but are not considered in the redirect, though
                    context.Response.Redirect(new PathString($"/{siteContext.UrlSegmentValue}").Add(context.Options.AccessDeniedPath) + returnUrl);
                    return(Task.CompletedTask);
                };
                options.Events.OnRedirectToLogin = context =>
                {
                    var returnUrl = "?ReturnUrl=" + context.Request.Path + context.Request.QueryString;
                    // fires with [Authorize] attribute, when the user is not authenticated
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    // other context properties can be set, but are not considered in the redirect, though
                    context.Response.Redirect(new PathString($"/{siteContext.UrlSegmentValue}").Add(context.Options.LoginPath) + returnUrl);
                    return(Task.CompletedTask);
                };
                options.Events.OnRedirectToLogout = context =>
                {
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    context.Response.Redirect(new PathString($"/{siteContext.UrlSegmentValue}").Add(context.Options.LogoutPath));
                    return(Task.CompletedTask);
                };
                options.Events.OnSignedIn += async context =>
                {
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    var success     = await siteContext.AppDb.UserRepository.SetLastLoginDateAsync(context.Principal.Identity.Name, null, CancellationToken.None);
                };
            });

            services.ConfigureExternalCookie(options =>
            {
                options.Cookie.HttpOnly    = true;
                options.Cookie.IsEssential = true;
                // MUST use the default options.CookieManager: After callback from social login, "organization" url segment will not be set,
                // because the CallbackPath from the provider will be sth. like "/signin-facebook" with cookie path to the same path.
                options.Cookie.Name       = ".ExtAuth.League";
                options.Cookie.Path       = "/";
                options.LoginPath         = new PathString("/account/sign-in");
                options.LogoutPath        = new PathString("/account/sign-in");
                options.AccessDeniedPath  = new PathString("/account/access-denied");
                options.Cookie.SameSite   = Microsoft.AspNetCore.Http.SameSiteMode.Lax; // don't use Strict here
                options.ExpireTimeSpan    = TimeSpan.FromDays(30);
                options.SlidingExpiration = true;

                options.Events.OnRedirectToAccessDenied = context =>
                {
                    var returnUrl = "?ReturnUrl=" + context.Request.Path + context.Request.QueryString;
                    // fires with [Authorize] attribute, when the user is authenticated, but does not have enough privileges
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    // other context properties can be set, but are not considered in the redirect, though
                    context.Response.Redirect(new PathString($"/{siteContext.UrlSegmentValue}").Add(context.Options.AccessDeniedPath) + returnUrl);
                    return(Task.CompletedTask);
                };
                options.Events.OnRedirectToLogin = context =>
                {
                    var returnUrl = "?ReturnUrl=" + context.Request.Path + context.Request.QueryString;
                    // fires with [Authorize] attribute, when the user is not authenticated
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    // other context properties can be set, but are not considered in the redirect, though
                    context.Response.Redirect(new PathString($"/{siteContext.UrlSegmentValue}").Add(context.Options.LoginPath) + returnUrl);
                    return(Task.CompletedTask);
                };
                options.Events.OnRedirectToLogout = context =>
                {
                    var siteContext = context.HttpContext.RequestServices.GetRequiredService <SiteContext>();
                    context.Response.Redirect(new PathString($"/{siteContext.UrlSegmentValue}").Add(context.Options.LogoutPath));
                    return(Task.CompletedTask);
                };
            });

            #endregion

            #region *** MailMergeLib as a service ***

            services.AddMailMergeService(
                options =>
            {
                options.Settings = Settings.Deserialize(
                    Path.Combine(WebHostEnvironment.ContentRootPath, Program.ConfigurationFolder,
                                 $@"MailMergeLib.{WebHostEnvironment.EnvironmentName}.config"),
                    System.Text.Encoding.UTF8);
                var fms = FileMessageStore.Deserialize(Path.Combine(WebHostEnvironment.ContentRootPath, Program.ConfigurationFolder,
                                                                    "MailMergeLibMessageStore.config"), System.Text.Encoding.UTF8);
                for (var i = 0; i < fms.SearchFolders.Length; i++)
                {
                    // make relative paths absolute - ready to use
                    fms.SearchFolders[i] = Path.Combine(WebHostEnvironment.WebRootPath, fms.SearchFolders[i]);
                }
                options.MessageStore = fms;
            });

            #endregion

            #region ** Timezone service per request **

            services.AddSingleton <NodaTime.TimeZones.DateTimeZoneCache>(sp =>
                                                                         new NodaTime.TimeZones.DateTimeZoneCache(NodaTime.TimeZones.TzdbDateTimeZoneSource.Default));

            var tzId = Configuration.GetSection("TimeZone").Value ?? "America/New York";
            // TimeZoneConverter will use the culture of the current scope
            services.AddScoped(sp => new Axuno.Tools.DateAndTime.TimeZoneConverter(
                                   sp.GetRequiredService <NodaTime.TimeZones.DateTimeZoneCache>(), tzId, CultureInfo.CurrentCulture,
                                   NodaTime.TimeZones.Resolvers.LenientResolver));

            #endregion

            #region ** Phone number service **

            services.AddSingleton <TournamentManager.DI.PhoneNumberService>(sp =>
                                                                            new PhoneNumberService(PhoneNumbers.PhoneNumberUtil.GetInstance()));

            #endregion

            services.AddSingleton <Helpers.MetaDataHelper>();

            services.Configure <CookiePolicyOptions>(options =>
            {
                // determines whether user consent for non-essential cookies is needed for a given request.
                options.CheckConsentNeeded    = context => false;
                options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
            });

            // expand search path to organization key sub-paths per HttpRequest
            services.Configure <RazorViewEngineOptions>(options =>
            {
                options.ViewLocationExpanders.Add(new Views.LeagueViewLocationExpander()); // R# will not resolve
                options.ViewLocationFormats.Add("/Views/Emails/{0}.cshtml");               // R# will resolve
            });

            #region *** Request Localization ***

            if (bool.Parse(Configuration.GetSection("CultureInfo:CulturePerRequest").Value))
            {
                var supportedCultures = new[]
                {
                    new CultureInfo("en"),
                    new CultureInfo("de")
                };

                services.Configure <RequestLocalizationOptions>(options =>
                {
                    options.DefaultRequestCulture = new RequestCulture(CultureInfo.DefaultThreadCurrentCulture);
                    // Formatting numbers, dates, etc.
                    options.SupportedCultures = supportedCultures;
                    // UI strings that we have localized.
                    options.SupportedUICultures = supportedCultures;
                    // e.g.: "en-US" => "en"
                    options.FallBackToParentCultures   = true;
                    options.FallBackToParentUICultures = true;

                    // Select the CookieRequestCultureProvider from the default RequestCultureProviders
                    // and set another cookie name than CookieRequestCultureProvider.DefaultCookieName
                    var cookieProvider = options.RequestCultureProviders
                                         .OfType <CookieRequestCultureProvider>()
                                         .FirstOrDefault();
                    if (cookieProvider != null)
                    {
                        cookieProvider.CookieName = ".PreferredLanguage";
                    }
                });
            }

            #endregion

            services.AddRouting(options =>
            {
                options.ConstraintMap.Add(OrganizationRouteConstraint.Name, typeof(OrganizationRouteConstraint));
                options.LowercaseQueryStrings = false;     // true does not work for UrlBase64 encoded strings!
                options.LowercaseUrls         = true;
                options.AppendTrailingSlash   = false;
            }
                                );

            services.AddRazorPages().AddRazorPagesOptions(options => {  });

            var mvcBuilder = services.AddMvc(options =>
            {
                options.EnableEndpointRouting = true;
                // Add model binding messages for errors that do not reach data annotation validation
                options.ModelBindingMessageProvider.SetValueMustNotBeNullAccessor(x => string.Format(Resources.ModelBindingMessageResource.ValueMustNotBeNull));
                options.ModelBindingMessageProvider.SetAttemptedValueIsInvalidAccessor((x, val) => string.Format(Resources.ModelBindingMessageResource.AttemptedValueIsInvalid, x, val));
                options.ModelBindingMessageProvider.SetValueIsInvalidAccessor(x => string.Format(Resources.ModelBindingMessageResource.ValueIsInvalid, x));
                options.ModelBindingMessageProvider.SetValueMustBeANumberAccessor(x => string.Format(Resources.ModelBindingMessageResource.ValueMustBeANumber, x));
                options.ModelBindingMessageProvider.SetMissingKeyOrValueAccessor(() => Resources.ModelBindingMessageResource.MissingKeyOrValue);
            })
                             .SetCompatibilityVersion(CompatibilityVersion.Latest)
                             .AddSessionStateTempDataProvider()
                             .AddDataAnnotationsLocalization()
                             .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
                             .AddMvcOptions(options =>
            {
                // Insert custom model binder providers before SimpleTypeModelBinderProvider
                options.ModelBinderProviders.Insert(0, new TimeSpanModelBinderProvider());
                options.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());
                // Replace ComplexTypeModelBinder with TrimmingModelBinder (trims all strings in models)
                options.ModelBinderProviders[options.ModelBinderProviders.TakeWhile(p => !(p is Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinderProvider)).Count()] = new ModelBinders.TrimmingComplexModelBinderProvider();
            })
                             .AddControllersAsServices();
#if DEBUG
            // Not to be added in production!
            if (WebHostEnvironment.IsDevelopment())
            {
                mvcBuilder.AddRazorRuntimeCompilation();
            }
#endif
            services.AddHttpsRedirection(options =>
            {
                options.RedirectStatusCode = StatusCodes.Status301MovedPermanently;
            });

            #region *** Add CloudScribeNavigation ***

            // CloudscribeNavigation requires:
            // ~/Views/Shared/NavigationNodeChildDropdownPartial.cshtml
            // ~/Views/Shared/NavigationNodeChildTreePartial.cshtml
            // ~/Views/Shared/NavigationNodeSideNavPartial.cshtml
            // ~/Views/Shared/Components/Navigation/*.cshtml
            // ~/Views/_ViewImports.cshtml: @using cloudscribe.Web.Navigation

            //services.AddCloudscribeNavigation(Configuration.GetSection("NavigationOptions"));
            services.AddCloudscribeNavigation(null);
            services.AddScoped <IOptions <NavigationOptions>, Navigation.LeagueSiteNavigationOptionsResolver>(); // resolve navigation xml files per organization
            services.AddScoped <INavigationTreeBuilder, Navigation.LeaguesNavigationTreeBuilder>();              //add top nav item for all leagues
            services.AddScoped <INavigationTreeBuilder, Navigation.InfosNavigationTreeBuilder>();                //add top nav item for info menu
            services.AddScoped <ITreeCache, Navigation.LeagueMemoryTreeCache>();                                 // cache navigation tree per organization
            #endregion

            #region *** HostedServices related ***

            services.AddSingleton <BackgroundWebHost>(sp => new BackgroundWebHost(services));
            services.Configure <BackgroundQueueConfig>(config => config.OnException = null);
            services.AddSingleton <IBackgroundQueue, BackgroundQueue>();
            services.AddConcurrentBackgroundQueueService();
            services.AddTransient <UserEmailTask>();
            services.AddTransient <FixtureEmailTask>();
            services.AddTransient <ResultEmailTask>();
            services.AddTransient <RankingUpdateTask>();
            services.AddTransient <ContactEmailTask>();
            services.AddTransient <TeamApplicationEmailTask>();

            #endregion
        }
        public void SerializeDeserialize()
        {
            var fms = new FileMessageStore(new[] { TestFileFolders.FilesAbsPath }, new[] { "Msg*.xml" }, Encoding.UTF8);

            Assert.AreEqual(fms, FileMessageStore.Deserialize(fms.Serialize()));
        }