Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

I want to cleanup and refactor this library (v40 proposal and discussion) #906

Closed
robinrodricks opened this issue Aug 5, 2022 · 147 comments
Closed

Comments

@robinrodricks
Copy link
Owner

robinrodricks commented Aug 5, 2022

Intro

Here are things that I'd like to cleanup going forward. Since this has ramifications on all existing projects using FluentFTP, and since these will be major breaking changes to the API, I'd like community feedback on these changes so that they are not hasty, disruptive, or out-of-touch with the actual userbase.

I would like these to be part of the v40 release so its easy for existing users to understand that v39 is the "legacy API" and that v40+ will use the "newer API". I have never undertaken such a large systematic refactoring before.

Topic 1

  • Problem: We have a lot of old/weird/unused functionality that is probably broken or not well tested or not being used.
    • EnableThreadSafeDataConnections - Crazy feature which spawns new FTP connections for each file transferred
    • QuickTransferLimit - Implemented but disabled
    • FtpClient.Connect(Uri) - Static constructors
    • FtpClient.GetPublicIP() - Static methods
    • DereferenceLink - does anyone even use this?
    • 200 different constructors for FtpClient - I would just like 1 or 2 constructors for common patterns.
  • Solution: Drop these features entirely.

Topic 2

  • Problem: We have a massive set of properties directly within the FtpClient class which clutters up its API.
  • Solution: Store all configuration setting properties into child classes whose references are stored within the FtpClient. This groups up the properties into logical categories and also eliminates them from the FtpClient API.
    • client.Connect() becomes client.Connect(FtpProfile)
    • client.FXPProgressInterval becomes client.FXP.ProgressInterval
    • client.DownloadZeroByteFiles becomes client.Download.ZeroByteFiles
    • client.TransferChunkSize becomes client.Download.ChunkSize and client.Upload.ChunkSize

Topic 3

  • Problem: Most FTP servers don't support directly downloading/uploading from an absolute path (eg: /root/animals/duck.txt, therefore we should always set the working directory to the folder (eg: /root/animals/) and then download the file (duck.txt). And we should not allow the user to manually modify the working directory because it would conflict with this system.
  • Solution: Drop support for GetWorkingDirectory and SetWorkingDirectory and instead always automatically set the working directory before file transfers or even getting the file size, etc. This would even fix issues where there are spaces in directory paths, and many other things. It would put us in parity with FileZilla.

Topic 4

  • Problem: Our logging is a mess. We should be able to integrate with NLog and other standard .NET loggers.
  • Solution: Drop support for FtpTrace and its outdated API and instead support a client.Logger property which can be set to our default logger or an NLog instance, etc.

Topic 5

  • Problem: Poor implementation of timezone conversion. Fixed implementation that does not allow users to customize it.
  • Solution: Implement a strategy pattern so users can select from:
    • NoTimeConversion
    • TimeZoneConversion - a new method that allows for DST to be observed (missing feature)
    • TimeOffsetConversion - users specify which is their PC timezone and the server timezone and we do simple math to convert it (current method)

Topic 6

  • Problem: We have 2 implementations of each function, a synchronous version and an async version. This causes bloat, mistakes/mismatches in the implementation, and weird behavior.
  • Solution: Drop support for synchronous API. Drop support for <.NET 3.5 as it doesn't support async.

Topic 7

  • Problem: Too much bloat with older IAsyncResult style async functions.
  • Solution: Remove it. Anyone who wants async can just use async/await.

Lets talk!

Please start the discussions below! Add your comments and mention which topic you are talking about. You can comment on any topic(s) that you like.

@Adhara3
Copy link
Contributor

Adhara3 commented Aug 5, 2022

Hi,

Foreword

as I already suggested in other issues, I would try, whenever possible, to avoid behaviour by enums, favoring a strategy pattern instead. This would have the benefit to help (not solve) Topic 2 for example, moving specific configuration only where necessary. To be clear I would favour something like new XxxFtpClient() which is an FTPClient of type Xxx ad internally it is built by injecting specific strategies for that server type. It's not easy but I see a potentially big architectural advantage.

Topic 3

This is the one I am mostly involved in. I agree that not allowing browsing the tree would help and that would also be strategy oriented. User provides the full path, it's up to the implementation use it as it is or navigate to the folder and then download

Topic 4

Having a simple IFtpClientLogger interface is a good way to go, you just need 5 or 6 logging methods and the user can easily plug in any implementation. Do not add any specific dependency.

Topic 6

I am not a huge fan of async code, to put it mild. But I get this is a feature largely used nowadays, the only thing I could say is that at the moment I'm only using sync methods from FluentFTP.

Hope this helps
A

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 6, 2022

Topic 3/4/5: Agreed (more or less)

Forward;

as I already suggested in other issues, I would try, whenever possible, to avoid behaviour by enums, favoring a strategy pattern instead.

It sounds pretty cool and could fix issues by offering simple and complex strategies. Assuming we have just one FtpClient for simplicity and all features are implemented in modular strategies, suggest the implementation. For example how would functionality be divided into strategies. For something like downloading a file, we don't really have strategies. Unless we should? Like SimpleFileDownload and ChunkedFileDownload etc. And what should the API look from the client? client.Downloader = new SimpleFileDownload()? Please suggest the concrete implementation so I can get a better idea.

@Adhara3
Copy link
Contributor

Adhara3 commented Aug 6, 2022

I'm not expert on FluentFTP client internals, so I'm still talking in a very theoretical way, based mainly on the Find What Is Varying and Encapsulate It basic principle.
So I would ideally try to have something like the following (pseudocode):

public class FTPClient
{
  public FTPClient(IDownloadStrategy downloadStrategy, IUploadStrategy uploadStrategy, IAuthenticationStrategy authStrategy)
  {
    // Of course, as many strategies as required
  }

  public void Download()
  {
    // You can du stuff here before calling the strategy, but it MUST be server independent, guaranteed
    m_downloadStrategy.Download();

    // Or the call could contain another strategy
   m_downloadStrategy.Download(m_authStrategy);

    /* This is a key point, strategies could have other strategies from constructor or at runtime, 
it really depends on the scenario. Passing at runtime may allow different behaviour, there is no 
good or bad, passing in construction gives the dev a message: this is readonly and cannot be 
changed, so depending on the scenario may be the right choice */
  }
}

public static class FritzBoxFtpClient
{
  // Here custom properties for this kind of client, should help with cluttered config
  // e.g. TransferChunkSize goes into the proper stategy
  public static FTPClient Create() 
  {
    return new FTPClient(); // Inject strategies for this specific client
  }
}

I do expect having, for each strategy type, a number N of implementations which is less than the actual number of clients, i.e. I guess a BasicDownloadStrategy may be reused for multiple clients.
I used the static factory method to avoid inheritance and because that would force to put all different behaviours into strategies and not in method overloading (mixing the two would kill code readability).

The other advantage is that this pattern would allow native extendibility. E.g. a user has a new client, maybe a custom one. He can create his own strategies and inject into FTPClient constructor and he should have something working. Basically, you would end up having some core classes + a bunch of add-ons (each client).

Moreover, async is in the syntax, so it doesn't work, but a Download strategy could be one file at a time and another implementation may be EnableThreadSafeDataConnections, basically instead of an option, it's a behaviour.
Topic 5 would also fall into this. You provide the API (i.e. the interface) and, if you like, a couple of very basic implementations, but then a user can always customize if required by injecting a different implementation.

The hard part is:

  • exact opposite of what you have today, instead of a huge single FTPClient class that does everything, several smaller classes. This helps with Unit testing, though
  • modularity for working properly needs to be as atomic as the smallest change, so it may end up being a bit iterative, e.g. IDownloadStrategy may not be enoght, you may need to split it (or to provide parts to it) like IPathBuildingStrategy or IServerResponseStrategy, just to give an idea

@FanDjango
Copy link
Collaborator

Just coming from my insular view for the IBM parts of this:

I am worried about:

Topic 3

Anyone writing a FTP Client GUI and wanting to give the user a way to walk the server-OS's file system will want to have these two functions, right? And the IBM parts really heavily use PWD/CWD.

Topic 6

I am not a huge fan of async code, to put it mild. But I get this is a feature largely used nowadays, the only thing I could say is that at the moment I'm only using sync methods from FluentFTP

Same for me.

I find it strange that C# religion goes for "not duplicating code" but makes it so difficult to have the same basic algorithm present in both sync and async modes. Certainly in an C(++) environment I could envision a single code set (using the preprocessor) that represents both flavors in a single source. How to do in C#?

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 7, 2022

Topic 3

Anyone writing a FTP Client GUI and wanting to give the user a way to walk the server-OS's file system will want to have these two functions, right? And the IBM parts really heavily use PWD/CWD.

Maybe we can have a strategy for this?

  • ManualDirectoryStrategy - the current get/set PWD/CWD
  • AutomaticDirectoryStrategy - a new one which can auto set the dir based on the file commands

Names are just suggestions, you can suggest better ones. I also don't like having the word "Strategy" as a suffix, is there a better naming scheme? <Variant> <Subject> "Strategy" seems like a very Java-esque too-many-words naming scheme and I would like a better one!

For backwards compatibility, we could support all the older constructors, which would be implemented like:

public FtpClient() {
	this.DownloadStrategy = new ChunkedDownloadStrategy();
	this.UploadStrategy = new ChunkedUploadStrategy();
	this.PathStrategy = new ManualDirectoryStrategy();
	....
}

Topic 6

I am not a huge fan of async code,

Same for me, however lots of contributors were excited to contribute async versions for some reason... And it makes the library look more "modern". I don't know why users can't just create a background worker/thread and use the sync API, maybe its because of the whole node.js "everything is an async/non-blocking operation" pattern that took the world by storm.

@Adhara3
Copy link
Contributor

Adhara3 commented Aug 7, 2022

This may be silly or very naif.
What about adding sync stuff as ExtensionMethods? That would allow keeping the class basically sync, users would be happy as they can call the method directly on the object.

A

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 7, 2022

This may be silly or very naif. What about adding sync stuff as ExtensionMethods? That would allow keeping the class basically sync, users would be happy as they can call the method directly on the object.

No, that would not solve the main problem of duplicating code.

@hez2010
Copy link
Contributor

hez2010 commented Aug 7, 2022

I would like to see

  1. Drop net3.5 support and keep only async variant of APIs, for those who want to use blocking sync call, they can always do FooAsync().ConfigureAwait(false).GetAwaiter().GetResult().
  2. Bringing IAsyncEnumerable support again to support "list streaming", without the dependency of Rx/Lx.Net.

@robinrodricks
Copy link
Owner Author

Bringing IAsyncEnumerable support again to support "list streaming", without the dependency of Rx/Lx.Net.

I'm not against it but it caused #703 and so it was reverted. If you have a solution for it without causing that issue, you can please do it and file a PR.

@Adhara3
Copy link
Contributor

Adhara3 commented Aug 7, 2022

No, that would not solve the main problem of duplicating code.

Ok but then this is why

I don't know why users can't just create a background worker/thread and use the sync API,

If they could, we also could in an extension method, right?

@robinrodricks
Copy link
Owner Author

I don't like providing main library API as extension methods. Extensions are meant when the third parties want to extend something without inheriting it.

@sierrodc
Copy link

sierrodc commented Aug 8, 2022

Bringing IAsyncEnumerable support again to support "list streaming", without the dependency of Rx/Lx.Net.

I'm not against it but it caused #703 and so it was reverted. If you have a solution for it without causing that issue, you can please do it and file a PR.

Hi,
According to #703, the issue was 'only' the reference to System.Linq.Async.

Please take a look here: https://github.com/RobertoSarati/FluentFTP_asyncenum. (fork reset to release 33.1.6).
As far as I can see the package System.Linq.Async is referenced only if ASYNCPLUS is defined, that is netstandard 2.0/2.1 and net5.0.
I've removed the package reference and without any other change I was able to compile the project. I've created a .net6 console app referencing the project and I'm able to use GetListingAsyncEnumerable without any issues. Maybe I'm missing the root cause (ps: I've removed net4, net45, net50 from csproj because I don't have them installed on my machine).

Btw, do you still want to support net3.5, net4, net45? According to microsoft .net framework < 4.6.2 are no more supported. So, what if target framework list is just [netstandard2.0;netstandard2.1;net50+]?

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 9, 2022

I've removed the package reference and without any other change I was able to compile the project. I

Ok that sounds cool @sierrodc , can you check if GetListingAsyncEnumerable works in https://www.nuget.org/packages/FluentFTP/39.2.0 ?

Btw, do you still want to support net3.5, net4, net45? According to microsoft .net framework < 4.6.2 are no more supported. So, what if target framework list is just [netstandard2.0;netstandard2.1;net50+]?

That is what I am asking the community. For myself I don't mind as I only use the latest .NET framework.

Unrelated:
Can I at least delete the older IAsyncResult style async functions from the library to remove bloat? Even I have never used them personally. Plus anyone who wants async can just use async/await.

@sierrodc
Copy link

sierrodc commented Aug 9, 2022

Can I at least delete the older IAsyncResult style async functions from the library to remove bloat? Even I have never used them personally. Plus anyone who wants async can just use async/await.

Imho the "sync/async" programming style should be as close as possible to what is today available in HttpClient so, for sure, no IAsyncResult. About sync method... I'm fine to have only aysnc/await (I'll use only these ones and I dont' want many methods in the intellisense dropdown), but if the community wants to keep sync style, I've no objections. But for me, we can switch to implement only async/await methods.
Later I'll check if GetListingAsyncEnumerable works in current master branch.

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 11, 2022

What I've planned for v40:

To delete

  1. EnableThreadSafeDataConnections
  2. QuickTransferLimit
  3. FtpClient.Connect(Uri)
  4. FtpClient.GetPublicIP()
  5. DereferenceLink
  6. DereferenceLinkAsync
  7. IAsyncResult functions (async implementation for .NET 2 and older)
  8. FtpTrace

To refactor:

  • client.Settings (class to hold all settings)

  • client.Logger (strategy pattern)

  • client.TimeConversion (strategy pattern)

    • NoTimeConversion
    • TimeZoneConversion
    • TimeOffsetConversion
  • Split FtpClient into 2 clients - FtpClient and AsyncFtpClient

    • Solves the issue of this messy API:
      image

To add:

  • client.Settings.ToJson - using System.Text.Json
  • client.Settings.FromJson
  • client.Settings.ToXml - using XmlSerializer
  • client.Settings.FromXml

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 11, 2022

Do you guys think this is a decent solution?

At present I don't see any advantage to the user for splitting the download/upload/FXP parts, because even if I did refactor the "downloader" to use strategy pattern, I don't have any alternate strategies to offer the user.

It would just force the user to change from: client.DownloadFile to client.Downloader.DownloadFile so that does not offer any major advantage at present.

@sierrodc
Copy link

1;3;4;7;8: ok
2;5;6: never used => ok

  • About logging: Such as HttpClient, why not just implement an EventSource? I think all libraries implement proper sinks (nlog, serilog ...). Really simple explanation here (it is the same implementation used by HttpClient and other .net libraries).
  • About TimeZone conversion: In my opinion the library should not handle conversions at all. There are other packages focused on this functionality. BUT if there are ftp servers that specify the timezone I think your solution is fine.
  • About AsyncClient and SyncClient: Fine for me.

@jnyrup
Copy link
Contributor

jnyrup commented Aug 11, 2022

First of all thanks for raising this discussion.

Topic 4: Logging

Building on top of the suggestion by @Adhara3 here's a rough prototype of how the logging could be structured

Define an interface in FluentFtp to separate the functionalities of FtpTrace.

interface IFluentFtpLogger
{
    void Log(FtpTraceLevel eventType, object message);
}

class NoOpLogger : IFluentFtpLogger
{
    public void Log(FtpTraceLevel eventType, object message)
    {
        // The default logger that does nothing.
    }
}

class ConsoleLogger : IFluentFtpLogger
{
    public void Log(FtpTraceLevel eventType, object message)
    {
        // Write to Console
        throw new NotImplementedException();
    }
}

class FileLogger : IFluentFtpLogger
{
    public void Log(FtpTraceLevel eventType, object message)
    {
        // Write to file
        throw new NotImplementedException();
    }
}

/// <summary>
/// Allows combining multiple loggers
/// </summary>
class CompositeLogger : IFluentFtpLogger
{
    private readonly IFluentFtpLogger[] m_loggers;

    public CompositeLogger(params IFluentFtpLogger[] loggers)
    {
        m_loggers = loggers;
    }

    public void Log(FtpTraceLevel eventType, object message)
    {
        foreach (var logger in m_loggers)
        {
            logger.Log(eventType, message);
        }
    }
}

To allow hooking into the widely used ILogger<T> interface from Microsoft.Extensions.Logging.Abstractions
we publish a separate package FluentFtp.Logging that simply contains an adapter to bridge between ILogger<T> and IFluentFtpLogger.
This avoids forcing a dependency on Microsoft.Extensions.Logging.Abstractions, but still makes it easy to integrate with popular logging libraries like Serilog, NLog, etc.

class LoggerAdapter : IFluentFtpLogger
{
    private readonly ILogger<IFtpClient> logger;

    public void Log(FtpTraceLevel eventType, object message)
    {
        var loglevel = ConvertLogLevel(eventType);
        if (logger.IsEnabled(loglevel))
        {
            logger.Log(loglevel, message.ToString());
        }
    }

    private static LogLevel ConvertLogLevel(FtpTraceLevel eventType)
    {
        return eventType switch
        {
            FtpTraceLevel.Verbose => LogLevel.Trace,
            FtpTraceLevel.Info => LogLevel.Information,
            FtpTraceLevel.Warn => LogLevel.Warning,
            FtpTraceLevel.Error => LogLevel.Error,
            _ => LogLevel.Information // Fallback
        };
    }
}

Topic 6: Target frameworks

FluentFtp currently supports frameworks that have been EOL for years.
Retaining that support seems to add significant complexity (currently 492 #ifs).

  • net20, net40 and net45 are no longer supported.
  • net35 SP1 is technically still supported until 2029, but I would recommend for FluentFtp to drop support for it.
  • .net 5 is no longer supported.

I we want to support older but still supported frameworks, we could go with net50;netstandard2.1;netstandard2.0;net472;net462.

https://docs.microsoft.com/en-us/dotnet/standard/library-guidance/cross-platform-targeting
dotnet/docs#8896
To see what TFM of FluentFtp an application will use, use Nuget Tools

Topic 6: Async

async is important in modern C# to avoid thread starvation, so I highly recommend not to drop that part.
It cannot always be delegated (nicely) to a background thread, e.g. a web api that as part of its work downloads from an FTP server.

In regards to whether we should also keep the sync APIs, other clients have mostly dropped sync versions.

  • HttpClient is mostly async - some sync APIs were added in .NET 6, but mainly for backwards compatibility or legacy code and because httpClient.Send() could be implemented better than httpClient.SendAsync().Wait().
  • AWS S3 client has transitioned to async only.

Topic 7: Removing IAsyncResult

🔥🔥🔥

Concurrency

One thing that surprised me when using FtpClient compared to e.g. HttpClient was that is inherently not suited for concurrent actions as it carries state, e.g. LastReply.

I haven't thought this through, but maybe something like this would di

var client = new FtpClient();

... // setup the ftp client 

using FtpConnection connection = client.CreateConnection();
byte[] content = await connect.DownloadAsync();

I'm aware that this would be yet another major restructuring of the project, so this is mostly a shower thought for now.

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 11, 2022

@jnyrup Thanks for the long and detailed answer!

Topic 4: Logging

I dislike "too many nuget packages" so would it cause any harm to add a direct dependency on Microsoft.Extensions.Logging.Abstractions ?

And then do as you proposed above. (NoOpLogger, FileLogger...) Maybe one change would be instead of having a single instance of logger, we could have a list of loggers directly in the client for simplicity? Making CompositeLogger not required.

API would be: client.Loggers = new List<ILogger{....} or client.AddLogger(myLogger)

Topic 6: Target frameworks

I agree (mostly):

Current frameworks:
net60;net50;net45;net40;net35;net20;netstandard1.4;netstandard1.6;netstandard2.0;netstandard2.1

Proposed frameworks:
net60;net50;net45;net40;net35;netstandard2.0;netstandard2.1

Topic 6: Async

I am planning on splitting FtpClient into 2 clients - FtpClient and AsyncFtpClient, do you agree with this? Both will inherit from BaseFtpClient which will have some reused properties/functions.

Concurrency

Yes concurrency is not currently supported. If you try to do multiple simultaneous operations on a single FTP client connection it will break (as per limitations of FTP protocol).

@Adhara3
Copy link
Contributor

Adhara3 commented Aug 12, 2022

I would probably drop the FtpTraceLevel way to go, I was thinking more to something like this

public interface IFtpLogger
{
  void Trace(string message); // I personally see no advantages in passing an object
  void Debug(string message);
  void Info(string message);
  void Warning(string message);
  void Error(string message);
  void Error(string message, Exception exception);
}

I would also probably provide only the composite and the NoOpLogger.
I also agree that if a dependency should be added, it should be added in a separate, dedicated package.

@robinrodricks

At present I don't see any advantage to the user for splitting the download/upload/FXP parts, because even if I did refactor the "downloader" to use strategy pattern, I don't have any alternate strategies to offer the user.
It would just force the user to change from: client.DownloadFile to client.Downloader.DownloadFile so that does not offer any major advantage at present.

I would not insist any further on this, I just wanted to clarify that:

  1. Strategy may not be the right term, but the idea is to hide different behaviours behind small classes to improve testability, favour aggregation and modularity and have a cleaner code. The idea is that there should be a "strategy" every time in current code you do if(serverType == xxx)
  2. It is an implementation detail, so users are not involved directly, FTPClient would act as a proxy for the strategies. Again, this would just be a more object oriented way of building the software.

Concurrency

This is interesting. As a user, the async interface would give me the natural feeling that the client is concurrent, so be careful here. As suggested by @jnyrup is a user expect to run the client as the HttpClient object, then it may be worth thinking a way to overcome this, maybe providing a FtpClientPool? Just as food for thoughts

A

@jnyrup
Copy link
Contributor

jnyrup commented Aug 12, 2022

Topic 4: Logging

I dislike "too many nuget packages" so would it cause any harm to add a direct dependency on Microsoft.Extensions.Logging.Abstractions ?

In terms of functionality of FluentFtp taking a direct dependency on M.E.L.A should "just work", but in general there are more considerations to take into account when adding extra dependencies.

When I'm about to add an additional nuget package, I have to evalute all transitive package references, in order to get a full view on licenses, vulnerabilities, maintenance status, etc. to avoid creating technical debt/security issues from day one.
For M.E.L.A this should not be a problem.

Increased size of your application as extra unused dlls are included.
For M.E.L.A it should be about 62K if FluentFtp is the only nuget referencing it.

Note, the earliest non-deprecated of this nuget package targets netstandard20 which is net461 or later.
So taking a direct dependency on Microsoft.Extensions.Logging.Abstractions prevents FluentFtp from targeting net35/net40/net45 (a non-issue to me since I would drop those targets).

So I think it mostly comes down to ones view on providing a single package with all batteries included or a more modular approach, where consumers don't have to depend on something they don't use.

And then do as you proposed above. (NoOpLogger, FileLogger...) Maybe one change would be instead of having a single instance of logger, we could have a list of loggers directly in the client for simplicity? Making CompositeLogger not required.

API would be: client.Loggers = new List<ILogger{....} or client.AddLogger(myLogger)

Established logging frameworks already provide knobs to combine multiple loggers (sinks) into a single ILogger, so the CompositeLogger should only be necessary when using basic included loggers FileLogger and ConsoleLogger.
We could even go in the more extremen direction and say implementations of how to log is not a responsibility of FluentFtp and not provide any logger, but only an interface and the NoOpLogger().

As an endorsement, the general "best-practice" is to use constructor-based dependency injection, but since a NoOpLogger can serve as a reasonable default property-based injection seems justified for FluentFtp.

So I would probably go with public XyzLoggingInterface Logger { get; set;} = new NoOpLogger();.

Two benefits of using the methods exposed on ILogger are the support for:

  • structured logging.
  • IsEnabled, which may make it cheaper when logging is disabled (e.g. having different loglevels between dev and prod).

Topic 6: Target frameworks

Proposed frameworks:
net60;net50;net45;net40;net35;netstandard2.0;netstandard2.1

I shouldn't be necessary to have both net50 and net60, unless you want to use net60 specific features or import different nuget packages based on the framework, as a net60 applications can consume a net50 nuget.
Adding unnecessarily targets to a nuget package just increases the binary size of the nuget package.

Out of curiosity, why do you intend to keep support for net35, net40 and net45?

Topic 6: Async

I am planning on splitting FtpClient into 2 clients - FtpClient and AsyncFtpClient, do you agree with this? Both will inherit from BaseFtpClient which will have some reused properties/functions.

I wouldn't do that.
It e.g. prevents reusing the same client for sync and async invocation paths.
I'm curious what the intended goal for this is.
If the intention is to reduce the number of available methods listed with intelliSense, you could consider creating two interfaces.

public interface IAsyncFtpClient
{
    public Task<byte> DownloadAsync();
}

public interface ISyncFtpClient
{
    public byte Download();
}

public class FtpClient : IAsyncFtpClient, ISyncFtpClient
{}

Strategies

The idea is that there should be a "strategy" every time in current code you do if(serverType == xxx)

I agree here that ideally, the FtpClient should not be aware of which server it is connected to, but just that it is entering server dependent behavior and hence delegate the implementation to a dedicated handler.

@robinrodricks
Copy link
Owner Author

robinrodricks commented Aug 15, 2022

Topic 4: Logging

Agreed on MELA.

I prefer "batteries included" as far as possible so I would like to implement all these in FluentFTP itself:

  • NullLogger
  • FileLogger
  • ConsoleLogger
  • CompositeLogger

Topic 6: Target frameworks

Out of curiosity, why do you intend to keep support for net35, net40 and net45?

Well just to support a broader user base. I was aware of enterprises that find it harder to upgrade to newer .NET versions. What is your take on this? If we are to support MELA then we would be forced to drop net 3.5/4/4.5 which is ok with me provided its ok with the user base.

I specifically wanted net5/6 so that users with those 2 frameworks can use FluentFTP without forcing you to upgrade to THE latest version. For example some businesses don't want to keep re-purchasing the latest VS version.

Topic 6: Async

I wouldn't do that. It e.g. prevents reusing the same client for sync and async invocation paths.

Would anyone do that? A lot of the users on this thread mentioned they use either sync or async but not both. I can't imagine how you would use both, considering that FluentFTP does not allow concurrent operations anyway. In fact I specifically don't like this method of mixing sync and async code as it may not be well tested usage.

I'm curious what the intended goal for this is.

  1. To clean up and reduce the API surface of each client class. I prefer less interfaces so that's not something I'd like to do.
  2. I would also like to remove the Async suffix for all class methods within the AsyncFtpClient class.
  3. Also to help cleanup the codebase as I will be splitting the files into separate code files for sync/async versions.

Is it ok to drop the 'Async' suffix for async methods?

From 1:

Of course, there are always exceptions to a guideline. The most notable one in the case of naming would be cases where an entire type’s raison d’etre is to provide async-focused functionality, in which case having Async on every method would be overkill, e.g. the methods on Task itself that produce other Tasks.

From 2:

If there is both non-async and async method that return the same thing I suffix the async one with Async. Otherwise not.

Strategies

I agree here that ideally, the FtpClient should not be aware of which server it is connected to, but just that it is entering server dependent behavior and hence delegate the implementation to a dedicated handler.

We already have implemented a server-specific strategy system which implements this as far as possible. Some zOS specific code is still remaining in the core which should be refactored out as far as possible.

@jnyrup
Copy link
Contributor

jnyrup commented Aug 15, 2022

Topic 6: Target frameworks

I was aware of enterprises that find it harder to upgrade to newer .NET versions. What is your take on this?

MS ended support for .net2.0 over 10 years ago and for net40 over 5 years ago.
net462 was introduced 5 years ago, so I would no longer regard it as a "newer .NET versions".
I admire your concern about not only supporting the latest and greatest (this a quite debated topic among OSS maintainers), but at one point most OSS projects drop some support in order to move forward and keep the project fun and maintainable.
So there no "right" thing here, e.g. Newtonsoft.Json still supports .net20.
Personally I would say that those "enterprises" can either use the current version of FluentFTP or have to upgrade their application to at least net462 to get the latest and greatest of FluentFTP.

I specifically wanted net5/6 so that users with those 2 frameworks can use FluentFTP without forcing you to upgrade to THE latest version. For example some businesses don't want to keep re-purchasing the latest VS version.

.NET5 is targeted in FluentFTP to add support for TLS 1.3.
But net6 applications can also consume that.
Adding a net6 target only increases the binary size of the FluentFTP nuget package as it includes a fluentFtp.dll for both net5 and net6.
Similarly, you don't have to add a net7 target when it is released in November, they can also consume a .net5 package.

Topic 6: Async

I think I understand your rationale now, to help users not mixing sync and async calls.
It doesn't sound unreasonable to me, but I can't recall having seen that design choice elsewhere.

@robinrodricks robinrodricks changed the title I want to cleanup and refactor this library, discussions are OPEN! I want to cleanup and refactor this library, discussions are OPEN! (v40) Sep 3, 2022
@robinrodricks robinrodricks changed the title I want to cleanup and refactor this library, discussions are OPEN! (v40) I want to cleanup and refactor this library (v40 proposal and discussion) Sep 3, 2022
@robinrodricks robinrodricks unpinned this issue Sep 3, 2022
@jnyrup
Copy link
Contributor

jnyrup commented Sep 3, 2022

I tried out 40.0.0-beta3 on the handful of different FTP servers we connect to, and I didn't notice any problems.
We use a mixture of FtpClient/AsyncFtpClient and the methods Connect, SetWorkingDirectory, GetListing, GetNameListing, DownloadFile, DownloadBytes, Disconnect.

@robinrodricks
Copy link
Owner Author

robinrodricks commented Sep 3, 2022

@jnyrup Awesome, thanks very much for testing!

I have released a nuget with your changes, I would appreciate if you could test again.

https://www.nuget.org/packages/FluentFTP/40.0.0-beta4

I added a note for all the contributors who made PRs against v40. Thanks guys!

@FanDjango Once you confirm that beta4 is good, I will release v40!

@FanDjango
Copy link
Collaborator

FanDjango commented Sep 3, 2022

@jnyrup Thanks for #940

This made it a bit of a pain point for me to contribute, as I had to disable auto-formatter when editing this project.

Same for me and this really helps

@FanDjango
Copy link
Collaborator

@robinrodricks I confirm 24dba70 works for my stuff

One thing disturbs me after this sequence of events: My test-suite uses a CLI calling the base of my app, it is a Console App, and I observe and check/analyze its output. My actual production app is a Win Forms GUI.

The test-suite (obviously) went a different compile path (#if...) on build and worked. This told me, all is well,

and then one of my clients, an important one but also one of the best - I love it when they pick up your updates and you get the feeling that they are "panting" for new stuff, reported this unexpected failure.

Do you have any ideas how to address this discrepancy?

I mean to say that the growing test suite is certainly also subject to this kind of divergence, not just mine.

Also, I cannot put a z/OS FTP server into a docker container - shouldn't we have a "sample" test unit for a "sample" real FTP server, of whatever kind. I would use that one...

@FanDjango
Copy link
Collaborator

FanDjango commented Sep 3, 2022

This business concerning Config.LogHost (is good, works) and Config.LogCredentials:

Apart from the details of the previous discussion, I see the following:

Look, example:

image

220....
331 Password required for <user>
230 User <user> logged on

You are going to see some stuff anyway. Configure the server, of course, but you cannot guarantee?

Wow, what privacy. But that's not my desire, to hide the user.

`Config.LogCredentials' (tries) to hide both, but then: see 331 and 230.

How can I enable logging of the user without showing the password? I would agree that the 230 is enough perhaps, but a user trying to make sure his logon is right, would like to see the "USER ...." command. Wireshark?

I mean, all I want to hide is the password?

Of course, I can "correct" that in my logger routine (whichever method I choose to use).

@robinrodricks
Copy link
Owner Author

robinrodricks commented Sep 4, 2022

My test-suite uses a CLI calling the base of my app, it is a Console App, and I observe and check/analyze its output. My actual production app is a Win Forms GUI.

Both are the same platform? .NET 4.7?

I love it when they pick up your updates and you get the feeling that they are "panting" for new stuff, reported this unexpected failure.

What failure?

#if

It is possible some are wrong, but I double checked it, and it looked ok.

I mean, all I want to hide is the password?

Lol. I will add back User/Pass config. I will try to hide mike as well.

@FanDjango
Copy link
Collaborator

Both are the same platform? .NET 4.7?

Oooops.

One thing disturbs me after this sequence of events...

After some checking, I am no longer disturbed.

@robinrodricks
Copy link
Owner Author

robinrodricks commented Sep 4, 2022

Oooops.

If some platform fails, I need to know.

@robinrodricks
Copy link
Owner Author

You are going to see some stuff anyway.

@FanDjango Done, please check the latest master - LogUserName & LogPassword added. mike should completely disappear.

@FanDjango
Copy link
Collaborator

If some platform fails, I need to know.

Yes, but you fixed it already, no longer a problem. But I found a problem on my side - testing with two different targets, a simple typo, which now both work, finally.

please check the latest master

Doing that in a moment

@FanDjango
Copy link
Collaborator

FanDjango commented Sep 4, 2022

Latest master works for me. LogUserName and LogPassword: works also

@robinrodricks
Copy link
Owner Author

Should I release the v40 nuget @FanDjango and @jnyrup ?

@jnyrup
Copy link
Contributor

jnyrup commented Sep 4, 2022

I have only a few minor comments left, but only the first one might be blocking v40 as it changes the API.

From the C# Design Guidelines

❌ DO NOT provide instance fields that are public or protected.
You should provide properties for accessing fields instead of making them public or protected.

There are 77 violations of that guideline

list of files
FluentFTP\Helpers\FtpListParser.cs:17
FluentFTP\Helpers\FtpListParser.cs:26
FluentFTP\Helpers\FtpListParser.cs:31
FluentFTP\Helpers\FtpListParser.cs:36
FluentFTP\Helpers\FtpListParser.cs:45
FluentFTP\Model\FtpClientState.cs:14
FluentFTP\Model\FtpClientState.cs:21
FluentFTP\Model\FtpClientState.cs:27
FluentFTP\Model\FtpClientState.cs:32
FluentFTP\Model\FtpClientState.cs:37
FluentFTP\Model\FtpClientState.cs:42
FluentFTP\Model\FtpClientState.cs:47
FluentFTP\Model\FtpClientState.cs:52
FluentFTP\Model\FtpClientState.cs:57
FluentFTP\Model\FtpFxpSession.cs:15
FluentFTP\Model\FtpFxpSession.cs:20
FluentFTP\Model\FtpFxpSession.cs:25
FluentFTP\Model\FtpFxpSessionAsync.cs:17
FluentFTP\Model\FtpFxpSessionAsync.cs:22
FluentFTP\Model\FtpFxpSessionAsync.cs:27
FluentFTP\Model\FtpListItem.cs:51
FluentFTP\Model\FtpListItem.cs:56
FluentFTP\Model\FtpListItem.cs:61
FluentFTP\Model\FtpListItem.cs:81
FluentFTP\Model\FtpListItem.cs:86
FluentFTP\Model\FtpListItem.cs:93
FluentFTP\Model\FtpListItem.cs:98
FluentFTP\Model\FtpListItem.cs:103
FluentFTP\Model\FtpListItem.cs:108
FluentFTP\Model\FtpListItem.cs:113
FluentFTP\Model\FtpListItem.cs:118
FluentFTP\Model\FtpListItem.cs:123
FluentFTP\Model\FtpListItem.cs:128
FluentFTP\Model\FtpListItem.cs:133
FluentFTP\Model\FtpListItem.cs:138
FluentFTP\Model\FtpListItem.cs:144
FluentFTP\Model\FtpListItem.cs:149
FluentFTP\Model\FtpListItem.cs:155
FluentFTP\Model\FtpListItem.cs:161
FluentFTP\Model\FtpListItem.cs:168
FluentFTP\Model\FtpProfile.cs:21
FluentFTP\Model\FtpProfile.cs:26
FluentFTP\Model\FtpProfile.cs:31
FluentFTP\Model\FtpProfile.cs:36
FluentFTP\Model\FtpProfile.cs:41
FluentFTP\Model\FtpProfile.cs:46
FluentFTP\Model\FtpProfile.cs:51
FluentFTP\Model\FtpProfile.cs:56
FluentFTP\Model\FtpProfile.cs:61
FluentFTP\Model\FtpProfile.cs:66
FluentFTP\Model\FtpResult.cs:15
FluentFTP\Model\FtpResult.cs:20
FluentFTP\Model\FtpResult.cs:25
FluentFTP\Model\FtpResult.cs:30
FluentFTP\Model\FtpResult.cs:45
FluentFTP\Model\FtpResult.cs:50
FluentFTP\Model\FtpResult.cs:55
FluentFTP\Model\FtpResult.cs:60
FluentFTP\Model\FtpResult.cs:65
FluentFTP\Model\FtpSizeReply.cs:10
FluentFTP\Model\IntRef.cs:7
FluentFTP\Rules\FtpFileExtensionRule.cs:17
FluentFTP\Rules\FtpFileExtensionRule.cs:22
FluentFTP\Rules\FtpFIleNameRegexRule.cs:19
FluentFTP\Rules\FtpFIleNameRegexRule.cs:24
FluentFTP\Rules\FtpFileNameRule.cs:16
FluentFTP\Rules\FtpFileNameRule.cs:21
FluentFTP\Rules\FtpFolderNameRegexRule.cs:19
FluentFTP\Rules\FtpFolderNameRegexRule.cs:24
FluentFTP\Rules\FtpFolderNameRegexRule.cs:29
FluentFTP\Rules\FtpFolderNameRule.cs:13
FluentFTP\Rules\FtpFolderNameRule.cs:24
FluentFTP\Rules\FtpFolderNameRule.cs:29
FluentFTP\Rules\FtpFolderNameRule.cs:34
FluentFTP\Rules\FtpSizeRule.cs:16
FluentFTP\Rules\FtpSizeRule.cs:21
FluentFTP\Rules\FtpSizeRule.cs:26

FtpSocketStream.SslStream is unused

Unused return values

Use the return value of method 'Trim'.	FluentFTP\Helpers\Parsers\NonStopParser.cs:59
Use the return value of method 'Parse'.	FluentFTP\Helpers\Parsers\VMSParser.cs:96

@robinrodricks
Copy link
Owner Author

robinrodricks commented Sep 4, 2022

DO NOT provide instance fields that are public or protected

Yes I never understood why C# forced the slower properties to be used instead of public fields. It actually has a negative effect on run time performance (may be negligible). Most of the uses here are just plain POCOs which don't really need properties.

@jnyrup
Copy link
Contributor

jnyrup commented Sep 4, 2022

A benefit of properties is that in debug mode, you can set breakpoints on them.

In release mode the call to the property is inlined, so the generated assembly is identical whether you use a field with or without a an explicit backing field.

SharpLab

@robinrodricks
Copy link
Owner Author

robinrodricks commented Sep 4, 2022

@jnyrup Ok, agreed that we should follow guidelines as it makes the lib more professional.

I fixed all issues except for the last 2 (return values), they are valid, or I cannot figure it out.

Release https://www.nuget.org/packages/FluentFTP/40.0.0-beta5

Guys please check and confirm it works, so I can release the final nuget.

@FanDjango
Copy link
Collaborator

A benefit of properties is that in debug mode, you can set breakpoints on them.

Something one takes for granted until you need to find out from whence came an update, or can you not break-point on a value (i.e. a field) changing its value?

In release mode the call to the property is inlined, so the generated assembly is identical whether you use a field with or without a an explicit backing field.

This is a profoundly valuable statement that reconciles me with the tediousness of making "quasi" unneeded properties.

@FanDjango
Copy link
Collaborator

FanDjango commented Sep 4, 2022

@robinrodricks Newest master works for me

One point:

If I compare, file by file, the Async client with the Sync client, there are subtle differences in the code. See the following screenshot of ExamDiff on Authenticate.cs just a an example.

image

There are actually a few of these, some only the comments, but some more than just that. Worth looking at, file by file (61 files)?

@FanDjango
Copy link
Collaborator

FanDjango commented Sep 4, 2022

Should I release the v40 nuget

I am just a single one of your users. Who am I to decide. You are raring to go and the changes are sensible, the code is much nicer this way, it becomes much easier (See my "ExamDiff" on previous post) to see any SYNC-ASYNC discrepancies, much less "#if"finess in the code.

I needed a small amount of code modifications. Users might gripe and grouch, but they gain a logging framework or can use legacy logging, they get cleaner code leading to better maintenance. If you follow through with handling their issues, they will see the upside of the V39-V40 switch.

So ok, go for it... IMHO

So, I am finished with this thread and will move on to other topics on my Todo-List. I thank all for the fruitful discussions and I learned a lot also.

@robinrodricks
Copy link
Owner Author

robinrodricks commented Sep 5, 2022

Who am I to decide. You are raring to go and the changes are sensible, the code is much nicer this way, it becomes much easier (See my "ExamDiff" on previous post) to see any SYNC-ASYNC discrepancies, much less "#if"finess in the code.

Thank you!

If I compare, file by file, the Async client with the Sync client, there are subtle differences in the code. See the following screenshot of ExamDiff on Authenticate.cs just a an example.

Seems like a bug.

There are actually a few of these, some only the comments, but some more than just that. Worth looking at, file by file (61 files)?

You can go ahead if you have time, sorry I won't have time this week. Beyond Compare would make quick work of it.

I am just a single one of your users. Who am I to decide. .. So ok, go for it... IMHO

I'll wait some days and then release.

@robinrodricks
Copy link
Owner Author

@jnyrup

In release mode the call to the property is inlined, so the generated assembly is identical whether you use a field with or without a an explicit backing field.

I was not aware of this.

@robinrodricks
Copy link
Owner Author

Published v40!

https://www.nuget.org/packages/FluentFTP/40.0.0

Now to wait for the coming storm (of issues!)

Thanks to ALL who participated in this thread. I am closing it, we can take the remaining features or any new issues as part of new discussions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

7 participants